diff --git a/Config/Schema/schema.xml b/Config/Schema/schema.xml index d78fd3c..1e39027 100644 --- a/Config/Schema/schema.xml +++ b/Config/Schema/schema.xml @@ -33,10 +33,14 @@ REFERENCES cm_co_dashboard_widgets(id) - - - - + + + + + + + + diff --git a/Controller/CoGrouperLitesController.php b/Controller/CoGrouperLitesController.php index 57b143a..ccbaa42 100644 --- a/Controller/CoGrouperLitesController.php +++ b/Controller/CoGrouperLitesController.php @@ -31,7 +31,7 @@ public function display($id) { $this->set('pl_grouperlite_index_url', Router::url([ 'plugin' => "grouper_lite", - 'controller' => 'GrouperGroups', + 'controller' => 'grouper_groups', 'action' => 'index', 'co' => $this->cur_co['Co']['id'], 'glid' => $id diff --git a/Controller/GrouperGroupsController.php b/Controller/GrouperGroupsController.php index 69fc296..03f0146 100644 --- a/Controller/GrouperGroupsController.php +++ b/Controller/GrouperGroupsController.php @@ -27,6 +27,8 @@ App::uses('Validator', 'Vendor/cakephp/Validation'); App::uses('CoGrouperLite', 'GrouperLite.Model/'); +App::uses('GrouperGroup', 'GrouperLite.Model/'); +App::uses('Identifier', 'Model'); /** * Class GrouperGroupsController @@ -36,33 +38,38 @@ class GrouperGroupsController extends GrouperLiteAppController { public $helpers = array('Html', 'Form', 'Flash'); - public $components = array('Flash', 'Paginator'); + public $uses = array('GrouperLite.GrouperGroup', 'CoPerson'); + public $components = array('Flash', 'Paginator', 'RequestHandler', 'Security' => array( + 'validatePost' => false, + 'csrfUseOnce' => false + )); public $name = 'GrouperGroups'; - //Unfortunately we cannot go below 20 since that is the default and if change to below 20 will not show - //page navigation unless change record count and go back to lower number on Display record selection!! Sucks! - public $paginate = array( - //Default records per page. - 'limit' => 20, - 'maxlimit' => 100, - 'page' => 1 - ); - - /** * Overrides parent beforeFilter to verify that Session contains the correct API settings. * * @return CakeResponse|void|null * */ - public function beforeFilter() { + public function beforeFilter() + { parent::beforeFilter(); + $this->Security->unlockedActions = array( + 'removeSubscriber', + 'addSubscriber', + 'joinGroup', + 'leaveGroup', + 'groupMember', + 'groupOptin', + 'groupOwner' + ); + //Need to find which plugin instance choosing, if more than one from cm_co_grouper_lites // table being used in COmanage. $grouperConnData = $this->Session->read('Plugin.Grouper.Api'); - if ($this->Session->check('Plugin.Grouper.Api.id') && count($grouperConnData) == 5) { + if ($this->Session->check('Plugin.Grouper.Api.id') && count($grouperConnData) == 10) { if (isset($this->passedArgs['glid'])) { if ($this->Session->read('Plugin.Grouper.Api.id') !== $this->passedArgs['glid']) { $this->setConnection(); @@ -77,115 +84,373 @@ public function beforeFilter() { $this->setConnection(); } - //Need to verify if user is part of + //Also check for CO of Organization + if (!$this->Session->check('Plugin.Grouper.Api.co')) { + $this->Session->write('Plugin.Grouper.Api.co', $this->passedArgs['co']); + } } /** * Adding Grouper Conn info to SESSION for use in Lib/GrouperApiAccess.php */ - private function setConnection() { + private function setConnection() + { $this->Session->write('Plugin.Grouper.Api.id', $this->passedArgs['glid']); + $this->Session->write('Plugin.Grouper.Api.co', $this->passedArgs['co']); //Now get the setup Dasboard instance from db for connection info. $getConnInfo = new CoGrouperLite(); $connectionInfo = $getConnInfo->findById($this->passedArgs['glid']); - $this->Session->write('Plugin.Grouper.Api.url', $connectionInfo['CoGrouperLite']['connUrl']); - $this->Session->write('Plugin.Grouper.Api.version', $connectionInfo['CoGrouperLite']['connVer']); - $this->Session->write('Plugin.Grouper.Api.user', $connectionInfo['CoGrouperLite']['connUser']); - $this->Session->write('Plugin.Grouper.Api.pass', $connectionInfo['CoGrouperLite']['connPass']); + $this->Session->write('Plugin.Grouper.Api.url', $connectionInfo['CoGrouperLite']['conn_url']); + $this->Session->write('Plugin.Grouper.Api.version', $connectionInfo['CoGrouperLite']['conn_ver']); + $this->Session->write('Plugin.Grouper.Api.user', $connectionInfo['CoGrouperLite']['conn_user']); + $this->Session->write('Plugin.Grouper.Api.pass', $connectionInfo['CoGrouperLite']['conn_pass']); + $this->Session->write('Plugin.Grouper.Api.grouperUrl', $connectionInfo['CoGrouperLite']['grouper_url']); + + $this->Session->write('Plugin.Grouper.Api.adHocHeading', $connectionInfo['CoGrouperLite']['adhoc_heading']); + $this->Session->write('Plugin.Grouper.Api.wgHeading', $connectionInfo['CoGrouperLite']['wg_heading']); + $this->Session->write('Plugin.Grouper.Api.defaultCollapse', $connectionInfo['CoGrouperLite']['default_collapse']); + } + + private function getConfig($page = 'groupmember') + { + $this->set('title', _txt('pl.grouperlite.title.groupmember')); + + $config = [ + "grouperbaseurl" => $this->Session->read('Plugin.Grouper.Api.grouperUrl'), + "isuserowner" => $this->GrouperGroup->isUserOwner($this->userId), + "isTemplateUser" => $this->GrouperGroup->isTemplateUser($this->userId), + "isGrouperVisible" => $this->GrouperGroup->isGrouperVisible($this->userId), + "defaultCollapse" => CakeSession::read('Plugin.Grouper.Api.defaultCollapse'), + "adHocHeading" => CakeSession::read('Plugin.Grouper.Api.adHocHeading'), + "wgHeading" => CakeSession::read('Plugin.Grouper.Api.wgHeading'), + 'co' => CakeSession::read('Plugin.Grouper.Api.co'), + 'glid' => $this->Session->read('Plugin.Grouper.Api.id'), + 'view' => $page + ]; + + return $config; } /** - * No true Index page, so sent to default page of Optin + * No true Index page, so sent to default page of My Membership * - * @return CakeResponse Redirect to Optin page + * @return CakeResponse Redirect to MyMembership page */ - public function index() { - return $this->redirect( - array('controller' => 'GrouperGroups', 'action' => 'groupoptin') - ); + public function index() + { + $this->set('config', $this->getConfig('groupmember')); + } + + public function groupMember() + { + $this->set('config', $this->getConfig('groupmember')); + $this->render('index'); + } + + public function groupOptin() + { + $this->set('config', $this->getConfig('groupoptin')); + $this->render('index'); + } + + public function groupOwner() + { + $this->set('config', $this->getConfig('groupowner')); + $this->render('index'); } /** - * Display of Grouper Group Information, such as Group Properties, Members and Attributes + * Show all members of a group + * Called from all pages via AJAX call * */ - public function groupInfo() { - $name = urldecode($this->request->query['groupname']); + public function groupSubscribers() + { + $groupName = urldecode($this->request->query['groupname']); - $this->set('isuserowner', $this->GrouperGroup->isUserOwner($this->userId)); + if ($this->request->is('ajax')) { + $this->response->disableCache(); + } - $this->set('title', _txt('pl.grouperlite.title.groupinfo')); + //Need to see if coming from AdHoc or from a WG (Working Group) + if (strpos($groupName, ':') === false) { + $groupNameFormatted = 'ref:incommon-collab:' . $groupName . ':users'; + } else { + $groupNameFormatted = $groupName; + } + + //Set initial + $scope = [ + 'groupName' => $groupNameFormatted + ]; try { - $details = $this->GrouperGroup->groupDescriptions($name); - $this->set('groupergroupsdetail', $details[0]); + $subscribers = $this->GrouperGroup->membersInGroup($scope, $this->userId); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': ' . var_export($e->getMessage(), true)); - $this->set('groupergroupsdetail', array()); - $this->Flash->set(_txt('pl.grouperlite.message.flash.info-group-failed'), array('key' => 'error')); + $this->Flash->set(_txt('pl.grouperlite.message.flash.group-detail-members-failed'), array('key' => 'error')); + } + + if(count($subscribers) < 1){ + $this->response->type('json'); + $this->response->statusCode(404); + //$this->response->body(json_encode(array('status' => 'ERROR', 'message' => 'NO ACCESS'))); + $this->response->send(); + $subscribers = ''; + } elseif (count($subscribers) == 1 && $subscribers[0]['sourceId'] == "NoAccess") { + $this->response->type('json'); + $this->response->statusCode(403); + //$this->response->body(json_encode(array('status' => 'ERROR', 'message' => 'NO ACCESS'))); + $this->response->send(); + $subscribers = ''; } + $this->set(compact('subscribers')); + $this->set('_serialize', 'subscribers'); + + } + + /** + * Add a new member to a group + * Called from all pages via AJAX call + * + */ + public function addSubscriber() + { + $groupName = urldecode($this->request->query['group']); + $addUserId = urldecode($this->request->query['userId']); + + if ($this->request->is('ajax')) { + $this->response->disableCache(); + } + + //Need to see if coming from AdHoc or from a WG (Working Group) + if (strpos($groupName, ':') === false) { + $groupNameFormatted = 'ref:incommon-collab:' . $groupName . ':users'; + } else { + $groupNameFormatted = $groupName; + } + + //Set initial + $scope = [ + 'groupName' => $groupNameFormatted, + 'addUserId' => $addUserId + ]; try { - $groupMembers = $this->membersInGroup(); - $this->set('groupergroupssubscribers', $groupMembers); + $resultAdd = $this->GrouperGroup->addMemberToGroup($scope, $this->userId); + } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': ' . var_export($e->getMessage(), true)); + $subscribers = 'ERROR'; - $this->set('groupergroupssubscribers', array()); - $this->Flash->set(_txt('pl.grouperlite.message.flash.group-detail-members-failed'), array('key' => 'error')); + // $this->Flash->set(_txt('pl.grouperlite.message.flash.group-detail-members-failed'), array('key' => 'error')); } - $this->set('grouperbaseurl', $this->Session->read('Plugin.Grouper.Api.url')); + if ($resultAdd == 'SUCCESS') { + // Do nothing + } elseif ($resultAdd == 'EXCEPTION') { + $this->response->type('json'); + $this->response->statusCode(404); + $this->response->body(json_encode(array('status' => 'ERROR', 'message' => 'EXCEPTION'))); + $this->response->send(); + $resultAdd = ''; + } else { + $this->response->type('json'); + $this->response->statusCode(401); + $this->response->body(json_encode(array('status' => 'ERROR', 'message' => 'ERROR'))); + $this->response->send(); + $resultAdd = ''; + } + $this->set(compact('resultAdd')); + $this->set('_serialize', 'resultAdd'); + } /** - * Show all members of group in Grouper Group detail page - * Called from method GroupInfo + * The majority of this code is copied from the find() function in the CoPeopleController that was added by + * Arlen. Eventually this will become a component, so for now just tweaking the code needed for the Grouper plugin + * to work. * */ - public function membersInGroup() { - $groupName = urldecode($this->request->query['groupname']); + public function findSubscriber() + { + + if ($this->request->is('ajax')) { + $this->response->disableCache(); + } + + // $groupName = urldecode($this->request->query['group']); + $findUserId = urldecode($this->request->query['term']); + $co = urldecode($this->request->query['co']); + + $mode = urldecode($this->request->query['mode']); + + // jquery Autocomplete sends the search as url?term=foo + $coPersonIds = array(); + if(!empty($findUserId)) { + // Leverage model specific keyword search + + // Note EmailAddress and Identifier don't support substring search + foreach(array('Name', 'EmailAddress', 'Identifier') as $m) { + $hits = $this->CoPerson->$m->search($co, $findUserId, 25); + + foreach($hits as $hit) { + if ($hit['CoPerson']['status'] == 'A') { + $coPersonIds[] = $hit['CoPerson']['id']; + } + } + // $coPersonIds = array_merge($coPersonIds, Hash::extract($hits, '{n}.CoPerson.id')); + } + } + + $coPersonIds = array_unique($coPersonIds); + + // Look up additional information to provide hints as to which person is which. + // We only do this when there are relatively small numbers of results to + // avoid making a bunch of database queries early in the search. + + $matches = array(); + + if(count($coPersonIds) > 100) { + // We don't return large sets to avoid slow performance + + $this->response->type('json'); + $this->response->statusCode(401); + $this->response->body(json_encode(array('status' => 'ERROR', 'message' => 'Too Many Results'))); + $this->response->send(); + $matches = ''; + } else { + $people = $this->CoPerson->filterPicker($co, $coPersonIds, $mode); + $pickerEmailType = $this->Co->CoSetting->getPersonPickerEmailType($co); + $pickerIdentifierType = $this->Co->CoSetting->getPersonPickerIdentifierType($co); + $pickerDisplayTypes = $this->Co->CoSetting->getPersonPickerDisplayTypes($co); + + foreach($people as $p) { + $label = generateCn($p['Name'][0]); + $idArr = $p['Identifier']; + $emailArr = $p['EmailAddress']; + $email = ''; + $emailLabel = ''; + $id = ''; + $idLabel = ''; + + // Iterate over the email array + if(!empty($emailArr) && !empty($pickerEmailType)) { + if(!empty($pickerDisplayTypes)) { + $emailLabel = _txt('fd.extended_type.generic.label', array(_txt('fd.email_address.mail'), $pickerEmailType)); + } + else { + $emailLabel = _txt('fd.email_address.mail') . ': '; + } + foreach($emailArr as $e) { + if($e['type'] == $pickerEmailType) { + $email = $e['mail']; + break; + } + } + } + + // Set the identifier for display (and limit it to 30 characters max) + if(!empty($idArr[0]['identifier']) && !empty($pickerIdentifierType)) { + if(!empty($pickerDisplayTypes)) { + $idLabel = _txt('fd.extended_type.generic.label', array(_txt('fd.identifier.identifier'), $pickerIdentifierType)); + } + else { + $idLabel = _txt('fd.identifier.identifier') . ': '; + } + foreach($idArr as $i) { + if($i['type'] == $pickerIdentifierType) { + //$id = mb_strimwidth($i['identifier'], 0, 30, '...'); + $id = $i['identifier']; + break; + } + } + } + + $matches[] = array( + 'value' => $p['CoPerson']['id'], + 'label' => $label, + 'email' => $email, + 'emailLabel' => $emailLabel, + 'identifier' => $id, + 'identifierLabel' => $idLabel + ); + } + } + + $this->set(compact('matches')); + $this->set('_serialize', 'matches'); + } + + /** + * Remove a member from a group + * Called from all pages via AJAX call + * + */ + public function removeSubscriber() + { + $groupName = urldecode($this->request->query['group']); + $remUserId = urldecode($this->request->query['userId']); + + if ($this->request->is('ajax')) { + $this->response->disableCache(); + } + + //Need to see if coming from AdHoc or from a WG (Working Group) + if (strpos($groupName, ':') === false) { + $groupNameFormatted = 'ref:incommon-collab:' . $groupName . ':users'; + } else { + $groupNameFormatted = $groupName; + } //Set initial $scope = [ - 'groupName' => $groupName + 'groupName' => $groupNameFormatted, + 'remUserId' => $remUserId ]; - $details = []; - try { - $details = $this->GrouperGroup->membersInGroup($scope); - // $this->set('membersingroup', $details); + $resultRemove = $this->GrouperGroup->removeMemberToGroup($scope, $this->userId); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': ' . var_export($e->getMessage(), true)); - // $this->set('membersingroup', array()); - $this->Flash->set(_txt('pl.grouperlite.message.flash.group-detail-members-failed'), array('key' => 'error')); + //$this->Flash->set(_txt('pl.grouperlite.message.flash.group-detail-members-failed'), array('key' => 'error')); } - return $details; + if ($resultRemove == 'SUCCESS') { + // Do nothing + } else { + $this->response->type('json'); + $this->response->statusCode(404); + $this->response->body(json_encode(array('status' => 'ERROR', 'message' => 'ERROR'))); + $this->response->send(); + $resultRemove = ''; + } + $this->set(compact('resultRemove')); + $this->set('_serialize', 'resultRemove'); } - /** * Listing of all Grouper Groups owned/admin by User Or search those Grouper Groups */ - public function groupOwner() { - $this->set('title', _txt('pl.grouperlite.title.groupowner')); + public function groupOwnerApi() + { + if ($this->request->is('ajax')) { + $this->response->disableCache(); + } - //Set initial settings for pagination + //Set initial setting $scope = [ - 'userId' => $this->userId, - 'page' => ($this->passedArgs['page'] ? $this->passedArgs['page']: $this->paginate['page']), - 'limit' => ($this->passedArgs['limit'] ? $this->passedArgs['limit']: $this->paginate['limit']), + 'userId' => $this->userId ]; - if (isset($this->request->data['search'])) { - $searchCriteria = urldecode($this->request->data['search']); + if (isset($this->request->query['search'])) { + $searchCriteria = urldecode($this->request->query['search']); + $this->set('searchcriteria', $searchCriteria); try { @@ -194,13 +459,15 @@ public function groupOwner() { $scope['searchcriteria'] = $searchCriteria; $scope['searchpage'] = 'ownerGroups'; - $data = $this->Paginator->paginate('GrouperGroup', $scope); - $this->set('groupergroupsowner', $data); + $groupowners = $this->GrouperGroup->getSearchedGroups($scope); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ' Search: ' . var_export($e->getMessage(), true)); + $this->response->type('json'); + $this->response->statusCode(500); + $this->response->send(); + $this->set('groupsowners', array()); - $this->set('groupergroupsowner', array()); $this->Flash->set(_txt('pl.grouperlite.message.flash.owner-group-failed'), array('key' => 'error')); return; } @@ -208,20 +475,21 @@ public function groupOwner() { try { $scope['method'] = 'ownerGroups'; - $data = $this->Paginator->paginate('GrouperGroup', $scope); - $this->set('groupergroupsowner', $data); + $groupowners = $this->GrouperGroup->ownerGroups($scope); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': ' . var_export($e->getMessage(), true)); + $this->response->type('json'); + $this->response->statusCode(500); + $this->response->send(); + $this->set('groupsowners', array()); - $this->set('groupergroupsowner', array()); $this->Flash->set(_txt('pl.grouperlite.message.flash.owner-group-failed'), array('key' => 'error')); return; } - } - $this->set('isuserowner', $this->GrouperGroup->isUserOwner($this->userId)); - $this->set('isTemplateUser', $this->GrouperGroup->isTemplateUser($this->userId)); + $this->set(compact('groupowners')); + $this->set('_serialize', 'groupowners'); } /** @@ -229,18 +497,20 @@ public function groupOwner() { * This includes self-joined Optin Groups, as well as required Groups User cannot leave * */ - public function groupMember() { - $this->set('title', _txt('pl.grouperlite.title.groupmember')); + public function groupMemberApi() + { + if ($this->request->is('ajax')) { + $this->response->disableCache(); + } - //Set initial settings for pagination + //Set initial setting $scope = [ - 'userId' => $this->userId, - 'page' => ($this->passedArgs['page'] ? $this->passedArgs['page']: $this->paginate['page']), - 'limit' => ($this->passedArgs['limit'] ? $this->passedArgs['limit']: $this->paginate['limit']), + 'userId' => $this->userId ]; - if (isset($this->request->data['search'])) { - $searchCriteria = urldecode($this->request->data['search']); + if (isset($this->request->query['search'])) { + $searchCriteria = urldecode($this->request->query['search']); + $this->set('searchcriteria', $searchCriteria); try { @@ -248,15 +518,21 @@ public function groupMember() { $scope['method'] = 'getSearchedGroups'; $scope['searchcriteria'] = $searchCriteria; $scope['searchpage'] = 'filteredMemberOfGroups'; + $scope['ContainsWG'] = true; - $data = $this->Paginator->paginate('GrouperGroup', $scope); - $this->set('groupergroupmemberships', $data); + $data = $this->GrouperGroup->getSearchedGroups($scope); + + $finalData = $this->breakoutGroups($data); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ' Search: ' . var_export($e->getMessage(), true)); + $this->response->type('json'); + $this->response->statusCode(500); + $this->response->send(); + $this->set('groupmemberships', array()); + $this->set('wgmemberships', array()); $this->Flash->set("Your Search Group cannot be found, please try again later.", array('key' => 'error')); - $this->set('groupergroupmemberships', array()); return; } } else { @@ -264,36 +540,44 @@ public function groupMember() { //Add setting for Group Membership $scope['method'] = 'filteredMemberOfGroups'; - $data = $this->Paginator->paginate('GrouperGroup', $scope); - $this->set('groupergroupmemberships', $data); + $data = $this->GrouperGroup->filteredMemberOfGroups($scope); + + $finalData = $this->breakoutGroups($data); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': ' . var_export($e->getMessage(), true)); + $this->response->type('json'); + $this->response->statusCode(500); + $this->response->send(); + $this->set('groupmemberships', array()); + $this->set('wgmemberships', array()); $this->Flash->set("Your Member Group cannot be found, please try again later.", array('key' => 'error')); - $this->set('groupergroupmemberships', array()); return; } } - $this->set('isuserowner', $this->GrouperGroup->isUserOwner($this->userId)); - $this->set('isTemplateUser', $this->GrouperGroup->isTemplateUser($this->userId)); + + $this->set(compact('finalData')); + $this->set('_serialize', 'finalData'); } /** * Display all Groups a User can Join */ - public function groupOptin() { - $this->set('title', _txt('pl.grouperlite.title.groupoptin')); + public function groupOptinApi() + { + if ($this->request->is('ajax')) { + $this->response->disableCache(); + } - //Set initial settings for pagination + //Set initial setting $scope = [ - 'userId' => $this->userId, - 'page' => (isset($this->passedArgs['page']) ? $this->passedArgs['page']: $this->paginate['page']), - 'limit' => (isset($this->passedArgs['limit']) ? $this->passedArgs['limit']: $this->paginate['limit']), + 'userId' => $this->userId ]; - if (isset($this->request->data['search'])) { - $searchCriteria = urldecode($this->request->data['search']); + if (isset($this->request->query['search'])) { + $searchCriteria = urldecode($this->request->query['search']); + $this->set('searchcriteria', $searchCriteria); try { @@ -302,14 +586,16 @@ public function groupOptin() { $scope['searchcriteria'] = $searchCriteria; $scope['searchpage'] = 'optinGroups'; - $data = $this->Paginator->paginate('GrouperGroup', $scope); - $this->set('groupergroupoptin', $data); + $groupoptins = $this->GrouperGroup->getSearchedGroups($scope); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . 'Search: ' . var_export($e->getMessage(), true)); + $this->response->type('json'); + $this->response->statusCode(500); + $this->response->send(); + $this->set('groupoptins', array()); $this->Flash->set("Your Optin Group Search cannot be found, please try again later.", array('key' => 'error')); - $this->set('groupergroupoptin', array()); return; } } else { @@ -317,20 +603,24 @@ public function groupOptin() { //Add settings for optinGroups $scope['method'] = 'optinGroups'; - $data = $this->Paginator->paginate('GrouperGroup', $scope); - $this->set('groupergroupoptin', $data); + $groupoptins = $this->GrouperGroup->optinGroups($scope); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': ' . var_export($e->getMessage(), true)); + $this->response->type('json'); + $this->response->statusCode(500); + $this->response->send(); + $this->set('groupoptins', array()); + $this->Flash->set("An error occurred with the Optin Groups, please try again later.", array('key' => 'error')); - $this->set('groupergroupoptin', array()); return; } } - $this->set('isuserowner', $this->GrouperGroup->isUserOwner($this->userId)); - $this->set('isTemplateUser', $this->GrouperGroup->isTemplateUser($this->userId)); + $this->set(compact('groupoptins')); + $this->set('_serialize', 'groupoptins'); } + /** * Create a new Grouper Group via Grouper Template * Note: This is tightly coupled code to requirements, so view is hardcoded to reflect current reqs. Will need @@ -339,59 +629,25 @@ public function groupOptin() { * Editing via a template will not be supported in this version of the plugin - Bill Kaufman * */ - public function groupCreateTemplate() { - if ($this->request->is('post')){ - if(!$this->GrouperGroup->createGroupWithTemplate($this->userId, $this->request->data)){ - $this->Flash->set("Error in creating group!", array('key' => 'error')); - return $this->redirect(array('action' => 'groupoptin')); - } else { - $this->Flash->set("Success in creating group!", array('key' => 'success')); - return $this->redirect(array('action' => 'groupoptin')); - } - } - $this->set('title', _txt('pl.grouperlite.title.templatecreate')); - } - - /** - * Delete a Grouper Group via Grouper Template - * - */ - public function groupDeleteTemplate() { - if(!$this->GrouperGroup->deleteGroupWithTemplate($this->userId, $this->request->data)){ - $this->Flash->set("Error in deleting group!", array('key' => 'error')); - return $this->redirect(array('action' => 'groupoptin')); - } - - $this->set('title', _txt('pl.grouperlite.title.templatecreate')); - } + public function groupCreateTemplate() + { + if ($this->request->is('post')) { + try { + $status = $this->GrouperGroup->createGroupWithTemplate($this->userId, $this->request->data); + if ($status['status'] !== true) { + $this->Flash->set($status['message'], array('key' => 'error')); + } else { + $this->Flash->set("Success in creating group!", array('key' => 'success')); + return $this->redirect(array('action' => 'groupowner')); + } - public function groupCreate() { - if ($this->request->is('post')){ - if(!$this->GrouperGroup->createUpdateGroup($this->userId, $this->request->data)){ - $this->Flash->set("Error in creating group!", array('key' => 'error')); - return $this->redirect(array('action' => 'groupOwner')); - } else { - $this->Flash->set("Your Group has been created!", array('key' => 'success')); - return $this->redirect(array('action' => 'groupOwner')); + } catch (Exception $e) { + CakeLog::write('error', __METHOD__ . ': ' . var_export($e->getMessage(), true)); + $this->Flash->set(var_export($e->getMessage(), true), array('key' => 'error')); } } - - $this->set('title', _txt('pl.grouperlite.title.groupcreate')); - $this->set('grouperstems', $this->GrouperGroup->getOwnedStems($this->userId)); - } - - //TODO - Finish this call - public function groupDelete() { - if(!$this->GrouperGroup->deleteGroup($this->userId, $this->request->data)){ - $this->Flash->set("Error in deleting group!", array('key' => 'error')); - return $this->redirect(array('action' => 'groupOwner')); - } else { - $this->Flash->set("Your Group has been deleted!", array('key' => 'success')); - return $this->redirect(array('action' => 'groupOwner')); - } - - $this->set('grouperstems', $this->GrouperGroup->getOwnedStems($this->userId)); + $this->set('title', _txt('pl.grouperlite.title.templatecreate')); } @@ -400,26 +656,40 @@ public function groupDelete() { * * @return CakeResponse Redirect back to "Optin" page */ - public function joinGroup() { - if ($this->request->is('post')) { - $name = $this->request->data['GroupName']; + public function joinGroup() + { + $name = urldecode($this->request->query['GroupName']); + $display = urldecode($this->request->query['GroupDisplayName']); - try { - if ($this->GrouperGroup->joinGroup($this->userId, $name)) { - $this->Flash->set(_txt('pl.grouperlite.message.flash.join-group-success'), array('key' => 'success')); - } else { - $this->Flash->set(_txt('pl.grouperlite.message.flash.join-group-failed'), array('key' => 'error')); - } - } catch (Exception $e) { - CakeLog::write('error', __METHOD__ . ': ' . var_export($e->getMessage(), true)); - $this->Flash->set(_txt('pl.grouperlite.message.flash.join-group-error'), array('key' => 'error')); - } + if ($this->request->is('ajax')) { + $this->response->disableCache(); + } - } else { - $this->Flash->set(_txt('pl.grouperlite.message.flash.join-group-error')); + try { + if ($this->GrouperGroup->joinGroup($this->userId, $name)) { + /*$this->Flash->set( + _txt('pl.grouperlite.message.flash.join-group-success', array(filter_var($display, FILTER_SANITIZE_SPECIAL_CHARS))), + array('key' => 'success') + );*/ + $resultAdd = "Success"; + } else { + $this->response->type('json'); + $this->response->statusCode(401); + $this->response->body(json_encode(array('status' => 'ERROR', 'message' => 'NOT ADDED'))); + $this->response->send(); + $resultAdd = ''; + } + } catch (Exception $e) { + CakeLog::write('error', __METHOD__ . ': ' . var_export($e->getMessage(), true)); + $this->response->type('json'); + $this->response->statusCode(404); + $this->response->body(json_encode(array('status' => 'ERROR', 'message' => 'EXCEPTION'))); + $this->response->send(); + $resultAdd = ''; } - return $this->redirect(array('action' => 'groupoptin')); + $this->set(compact('resultAdd')); + $this->set('_serialize', 'resultAdd'); } /** @@ -427,26 +697,36 @@ public function joinGroup() { * * @return CakeResponse Redirect back to "Member Of" page */ - public function leaveGroup() { - if ($this->request->is('post')) { - $name = $this->request->data['GroupName']; + public function leaveGroup() + { + $name = urldecode($this->request->query['GroupName']); + $display = urldecode($this->request->query['GroupDisplayName']); - try { - if ($this->GrouperGroup->leaveGroup($this->userId, $name)) { - $this->Flash->set(_txt('pl.grouperlite.message.flash.leave-group-success'), array('key' => 'success')); - } else { - $this->Flash->set(_txt('pl.grouperlite.message.flash.leave-group-failed'), array('key' => 'error')); - } - } catch (Exception $e) { - CakeLog::write('error', __METHOD__ . ': ' . var_export($e->getMessage(), true)); - $this->Flash->set(_txt('pl.grouperlite.message.flash.leave-group-error'), array('key' => 'error')); - } + if ($this->request->is('ajax')) { + $this->response->disableCache(); + } - } else { - $this->Flash->set(_txt('pl.grouperlite.message.flash.leave-group-error'), array('key' => 'error')); + try { + if ($this->GrouperGroup->leaveGroup($this->userId, $name)) { + $resultRemove = "Success"; + } else { + $this->response->type('json'); + $this->response->statusCode(401); + $this->response->body(json_encode(array('status' => 'ERROR', 'message' => 'NOT DELETED'))); + $this->response->send(); + $resultRemove = ''; + } + } catch (Exception $e) { + CakeLog::write('error', __METHOD__ . ': ' . var_export($e->getMessage(), true)); + $this->response->type('json'); + $this->response->statusCode(404); + $this->response->body(json_encode(array('status' => 'ERROR', 'message' => 'EXCEPTION'))); + $this->response->send(); + $resultRemove = ''; } - return $this->redirect(array('action' => 'groupmember')); + $this->set(compact('resultRemove')); + $this->set('_serialize', 'resultRemove'); } /** @@ -460,34 +740,69 @@ public function leaveGroup() { * @return Array Permissions * @since COmanage Registry v3.2.0 */ - function isAuthorized() { + function isAuthorized() + { $roles = $this->Role->calculateCMRoles(); - //Need to pull in UserID for access to Grouper + /** + * The following code displays a few custom implementations of the + * login process used to crosswalk a user for Grouper authentication. + * + * You may need to further customize this section to meet your organization + * crosswalk needs. + */ + + /** + * Default when login-id is the same as grouper id + */ + // Default Begin =============================================== + + /* if ($this->Session->check('Auth.User.username')) { - $this->userId = $this->Session->read('Auth.User.username'); + $this->userId = $this->Session->read('Auth.User.username'); } + */ + // Default End =============================================== - // Determine what operations this user can perform + /** + * Customized Crosswalk from login-id to Grouper Username + */ + // Custom Begin =============================================== + + $username = $this->Session->read('Auth.User.username'); + if ($this->Session->check('Plugin.Grouper.UserId')) { + $this->userId = $this->Session->read('Plugin.Grouper.UserId'); + } else { + $uid = $this->getPersonIdFromUsername($username); + $this->userId = $this->getUserId($uid); + $this->Session->write('Plugin.Grouper.UserId', $this->userId); + } + + // Custom End =============================================== + + + // Determine what operations this user can perform // Construct the permission set for this user, which will also be passed to the view. //Note: Leaving in current format, in case need to restrict certain pages, can just remove true and add params. $p = array(); $p['index'] = true; - $p['groupinfo'] = true; - $p['membersInGroup'] = true; $p['groupowner'] = true; - $p['groupmember'] = true; + $p['groupownerapi'] = true; $p['groupoptin'] = true; - $p['emaillistsoptin'] = true; - $p['emaillistsmember'] = true; - $p['emaillistsmanage'] = true; - $p['emaillistsinfo'] = true; - $p['groupcreate'] = true; - $p['groupdelete'] = true; - $p['joingroup'] = true; - $p['leavegroup'] = true; + $p['groupoptinapi'] = true; + $p['groupmember'] = true; + $p['groupmemberapi'] = true; + $p['getBaseConfig'] = true; + $p['groupSubscribers'] = true; + $p['addSubscriber'] = true; + $p['findSubscriber'] = true; + $p['removeSubscriber'] = true; + + $p['groupCreate'] = true; + $p['joinGroup'] = true; + $p['leaveGroup'] = true; $p['groupcreatetemplate'] = true; $this->set('permissions', $p); @@ -495,97 +810,64 @@ function isAuthorized() { return ($p[$this->action]); } - public function emaillistsOptin() { - $this->set('title', _txt('pl.grouperlite.title.emaillists-join')); - - //Set initial settings for pagination - $scope = [ - 'userId' => $this->userId, - 'page' => (isset($this->passedArgs['page']) ? $this->passedArgs['page']: $this->paginate['page']), - 'limit' => (isset($this->passedArgs['limit']) ? $this->passedArgs['limit']: $this->paginate['limit']), - ]; - - try { - //Add settings for optinEmailLists - $scope['method'] = 'optinEmailGroups'; - - $data = $this->Paginator->paginate('GrouperGroup', $scope); - $this->set('emailgroups', $data); - - } catch (Exception $e) { - CakeLog::write('error', __METHOD__ . ': ' . var_export($e->getMessage(), true)); - $this->Flash->set("An error occurred with the Optin Groups, please try again later.", array('key' => 'error')); - $this->set('emailgroups', array()); - return; - } - - $this->set('isuserowner', $this->GrouperGroup->isUserOwner($this->userId)); - $this->set('isTemplateUser', $this->GrouperGroup->isTemplateUser($this->userId)); + private function getPersonIdFromUsername($username) + { + $args = array(); + $args['conditions']['Identifier.identifier'] = $username; + $args['conditions']['Identifier.status'] = SuspendableStatusEnum::Active; + $args['conditions']['Identifier.deleted'] = false; + $args['conditions']['Identifier.identifier_id'] = null; + $args['conditions']['NOT']['Identifier.co_person_id'] = null; + $args['conditions']['Identifier.type'] = 'eppn'; + $args['contain'] = false; + + $Identifier = new Identifier(); + $co_person_id = $Identifier->find('first', $args); + + return $co_person_id['Identifier']['co_person_id']; } - public function emaillistsMember() + private function getUserId($id) { - $this->set('title', _txt('pl.grouperlite.title.emaillists-member')); - - //Set initial settings for pagination - $scope = [ - 'userId' => $this->userId, - 'page' => ($this->passedArgs['page'] ? $this->passedArgs['page']: $this->paginate['page']), - 'limit' => ($this->passedArgs['limit'] ? $this->passedArgs['limit']: $this->paginate['limit']), - ]; - - try { - //Add setting for Group Membership - $scope['method'] = 'filteredMemberOfEmails'; + $args = array(); + $args['conditions']['Identifier.co_person_id'] = $id; + $args['conditions']['Identifier.type'] = 'I2CollabPN'; + $args['conditions']['Identifier.status'] = SuspendableStatusEnum::Active; + $args['contain'] = false; - $data = $this->Paginator->paginate('GrouperGroup', $scope); - $this->set('emailgroups', $data); - - } catch (Exception $e) { - CakeLog::write('error', __METHOD__ . ': ' . var_export($e->getMessage(), true)); + $Identifier = new Identifier(); + $grouper_identifier = $Identifier->find('first', $args); - $this->Flash->set("Your Member Group cannot be found, please try again later.", array('key' => 'error')); - $this->set('emailgroups', array()); - return; - } - - $this->set('isuserowner', $this->GrouperGroup->isUserOwner($this->userId)); - $this->set('isTemplateUser', $this->GrouperGroup->isTemplateUser($this->userId)); + return $grouper_identifier['Identifier']['identifier']; } + /** + * Breakout Working Groups from AdHoc Groups for display purposes in UI. + * + * @param array $recordSet + * @param string $type 'basic' = view in Member page, 'admin' = view in Admin page, 'optin' = View in Optin page + * @return array[] + * + */ + private function breakoutGroups(array $recordSet, $type = 'basic') + { + //TODO - May move this logic down into the GrouperGroup model file, once all is agreed upon. + $wgData = array(); + $notWGData = array(); + //Parse out the Working Groups from the Ad-hoc groups + foreach ($recordSet as $record) { + if (isset($record['WGName'])) { + $wgData[] = $record; + } else { + $notWGData[] = $record; + } + } - public function emaillistsManage() { - $this->set('title', _txt('pl.grouperlite.title.emaillists-manage')); - // mock data - $this->set('group', array( - 'member' => true, - 'name' => 'Email List 1', - 'domain' => 'internet2', - 'description' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - 'enabled' => 'T' - )); - - $this->set('isuserowner', $this->GrouperGroup->isUserOwner($this->userId)); - $this->set('isTemplateUser', $this->GrouperGroup->isTemplateUser($this->userId)); + return array( + 'adhoc' => $notWGData, + 'working' => $wgData + ); } - public function emaillistInfo() { - $this->set('title', _txt('pl.grouperlite.title.emaillistsinfo')); - // mock data - $this->set('groupergroupsdetail', array( - 'member' => true, - 'domain' => 'internet2', - 'uuid' => 'abc123xyz789', - 'displayExtension' => 'email-list-1', - 'extension' => 'email-list-1', - 'displayName' => 'Email List 1', - 'typeOfGroup' => 'list', - 'name' => 'Email List 1', - 'description' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - 'enabled' => 'T', - 'attributes' => array() - )); - $this->set('grouperbaseurl', $this->Session->read('Plugin.Grouper.Api.url')); - } -} +} \ No newline at end of file diff --git a/Lib/GrouperApiAccess.php b/Lib/GrouperApiAccess.php index bea20de..c2106d6 100644 --- a/Lib/GrouperApiAccess.php +++ b/Lib/GrouperApiAccess.php @@ -52,7 +52,8 @@ class GrouperApiAccess * * @throws GrouperLiteException If issue with Grouper WS connection */ - public function __construct() { + public function __construct() + { $this->http = new GrouperHTTPWrapper(); @@ -70,65 +71,23 @@ public function __construct() { $this->http->setPassword(CakeSession::read('Plugin.Grouper.Api.pass')); } - /** - * NOT BEING USED - * Get User information from Grouper Web Service - * - * @param array $queryData Array of conditions for querying - * @return array - * @throws Exception - */ - public function getGrouperUser(array $queryData) { - - $userId = $queryData['userId']; - $this->http->setHeader(array('Content-Type' => 'application/json', 'Accept' => 'application/json')); - - // Create attributes want returned from call to Grouper WS - $formArray = array( - 'WsRestGetSubjectsRequest' => array( - 'subjectAttributeNames' => array( - 'edupersonprincipalname', - 'uid', - 'mail', - 'cn', - 'givenname' - ), - 'wsSubjectLookups' => array( - array('subjectIdentifier' => $userId) - ) - ) - ); - $formData = json_encode($formArray); - $connectionUrl = $this->config['fullUrl'] . '/subjects'; - - try { - $results = $this->http->sendRequest('GET', $connectionUrl, $formData); - - // Parse out relevant records to send front end - if (isset($results['WsGetSubjectsResults']['wsSubjects'][0]['id']) && $results['WsGetSubjectsResults']['wsSubjects'][0]['id'] != NULL) { - return $results['WsGetSubjectsResults']['wsSubjects'][0]['id']; - } - - } catch (Exception $e) { - CakeLog::write('error', __METHOD__ . ': An error occurred'); - throw $e; - } - - return array(); - } - /** * Get Groups that User is a member of from Grouper. * + * Note: Params added at end make sure that the groups returned can only be viewed by the member logged into + * Grouper Lite + * * @param array $queryData Array of conditions for querying * @return array Membership records that User is a member of in Grouper * @throws GrouperLiteException */ - public function getGrouperMemberOfGroups(array $queryData) { + public function getGrouperMemberOfGroups(array $queryData) + { //Build request logic $userId = $queryData['userId']; - $connectionUrl = "{$this->config['fullUrl']}/subjects/{$userId}/groups"; + $connectionUrl = "{$this->config['fullUrl']}/subjects/$userId/groups?"; + $connectionUrl .= "wsLiteObjectType=WsRestGetGroupsLiteRequest&actAsSubjectId=$userId"; try { $results = $this->http->sendRequest('GET', $connectionUrl); @@ -138,7 +97,7 @@ public function getGrouperMemberOfGroups(array $queryData) { return $results['WsGetGroupsLiteResult']['wsGroups']; } } catch (Exception $e) { - CakeLog::write('error', __METHOD__ . ': An error occurred'); + CakeLog::write('error', __METHOD__ . ': An error occurred'); throw $e; } @@ -152,7 +111,8 @@ public function getGrouperMemberOfGroups(array $queryData) { * @return bool True if join or leave successful, False if not * @throws GrouperLiteException */ - public function grouperGroupLeaveOrJoin(array $queryData) { + public function grouperGroupLeaveOrJoin(array $queryData) + { $groupName = $queryData['groupName']; $userId = $queryData['userId']; @@ -168,8 +128,8 @@ public function grouperGroupLeaveOrJoin(array $queryData) { $resultResponse = 'WsAddMemberResults'; $resultGroup = 'wsGroupAssigned'; } else { - CakeLog::write('error',__METHOD__ . ": Option of $groupLeaveOrJoin is not supported"); - throw new GrouperLiteException("Receved option of $groupLeaveOrJoin which is not supported"); + CakeLog::write('error', __METHOD__ . ": Option of $groupLeaveOrJoin is not supported"); + throw new GrouperLiteException("Received option of $groupLeaveOrJoin which is not supported"); } //Build request logic @@ -186,7 +146,7 @@ public function grouperGroupLeaveOrJoin(array $queryData) { ); $this->http->setHeader(array('Content-Type' => 'application/json', 'Accept' => 'application/json')); - $connectionUrl = "{$this->config['fullUrl']}/groups/{$groupName}/members"; + $connectionUrl = "{$this->config['fullUrl']}/groups/$groupName/members"; try { $results = $this->http->sendRequest('PUT', $connectionUrl, json_encode($groupCommand)); @@ -198,7 +158,7 @@ public function grouperGroupLeaveOrJoin(array $queryData) { } } } catch (Exception $e) { - CakeLog::write('error', __METHOD__ . ': An error occurred'); + CakeLog::write('error', __METHOD__ . ': An error occurred'); throw $e; } @@ -206,17 +166,17 @@ public function grouperGroupLeaveOrJoin(array $queryData) { } /** - * Gets all available Optin groups in Grouper - * Checks to see which Optin groups the user is already a member of - * Returns Optin groups that can be joined and ones that the user is already joined + * Gets all available Optin/OptOut groups in Grouper + * + * Returns Optin/OptOut groups that can be joined/left * * @param array $queryData Array of conditions for querying * @return array Optin groups from Grouper * @throws GrouperLiteException * */ - public function getOptinGroups(array $queryData) { - $queryData['groupType'] = 'Optins'; + public function getOptionalGroups(array $queryData) + { try { $results = $this->useMembershipUrl($queryData); @@ -232,29 +192,73 @@ public function getOptinGroups(array $queryData) { } /** - * Gets all groups in Grouper where user is an admin/owner + * Gets all groups in Grouper where user is an admin/owner or has update privs * * @param array $queryData Array of conditions for querying * @return array Array of groups from Grouper * @throws GrouperLiteException * */ - public function getOwnedGroups(array $queryData) { - $queryData['groupType'] = 'Owner'; + public function getOwnedGroups(array $queryData) + { try { - $results = $this->useMembershipUrl($queryData); + $queryData['groupType'] = 'admin'; + $resultsAdmin = $this->useMembershipUrl($queryData); - if (isset($results['WsGetMembershipsResults']['wsGroups']) && $results['WsGetMembershipsResults']['wsGroups'] != NULL) { - return $results['WsGetMembershipsResults']['wsGroups']; + $queryData['groupType'] = 'update'; + $resultsUpdate = $this->useMembershipUrl($queryData); + + if (isset($resultsAdmin['WsGetMembershipsResults']['wsGroups']) && $resultsAdmin['WsGetMembershipsResults']['wsGroups'] != NULL) { + $admins = $resultsAdmin['WsGetMembershipsResults']['wsGroups']; + } else { + $admins = array(); + } + + if (isset($resultsUpdate['WsGetMembershipsResults']['wsGroups']) && $resultsUpdate['WsGetMembershipsResults']['wsGroups'] != NULL) { + $updaters = $resultsUpdate['WsGetMembershipsResults']['wsGroups']; + } else { + $updaters = array(); } + + return $this->removeDuplicates($admins, $updaters); } catch (Exception $e) { - CakeLog::write('error', __METHOD__ . ': An error occurred'); + CakeLog::write('error', __METHOD__ . ': An error occurred'); throw $e; } - return array(); } + /** + * Removes duplicates where the user is the owner and the updater of the group. Just one line instead of two. + * + * @param array $arrOne + * @param array $arrTwo + * @return array + */ + public function removeDuplicates(array $arrOne, array $arrTwo) + { + + //Determine which array is bigger and use as base + $countOne = count($arrOne); + $countTwo = count($arrTwo); + if ($countOne >= $countTwo) { + $arrL = $arrOne; + $arrS = $arrTwo; + } else { + $arrL = $arrTwo; + $arrS = $arrOne; + } + + foreach ($arrL as $large) { + foreach ($arrS as $key => $val) { + if ($large['uuid'] == $val['uuid']) { + unset($arrS[$key]); + } + } + } + + return array_merge_recursive($arrL, $arrS); + } /** * Get members associated to a specific Grouper Group @@ -263,32 +267,41 @@ public function getOwnedGroups(array $queryData) { * @return array Listing of Members belonging to Grouper Group * @throws GrouperLiteException */ - public function getMembersInGroup(array $queryData) { + public function getMembersInGroup(array $queryData) + { - //Build request logic - $usersToShow = array( - "WsRestGetMembersRequest" => array( - "wsGroupLookups" => array( - array("groupName" => $queryData['groupName']) - ), - "subjectAttributeNames" => array( - "name" + try { + //Build request logic + $usersToShow = array( + "WsRestGetMembersRequest" => array( + "actAsSubjectLookup" => array( + "subjectId" => $queryData['userId'] + ), + "wsGroupLookups" => array( + array("groupName" => $queryData['groupName']) + ), + "subjectAttributeNames" => array( + "name" + ) ) - ) - ); + ); - $this->http->setHeader(array('Content-Type' => 'application/json', 'Accept' => 'application/json')); - $connectionUrl = "{$this->config['fullUrl']}/groups"; + $this->http->setHeader(array('Content-Type' => 'application/json', 'Accept' => 'application/json')); + $connectionUrl = "{$this->config['fullUrl']}/groups"; - try { $results = $this->http->sendRequest('POST', $connectionUrl, json_encode($usersToShow)); // Parse out relevant records to send front end + if(isset($results['WsGetMembersResults']['results'][0]['resultMetadata']['resultCode']) && $results['WsGetMembersResults']['results'][0]['resultMetadata']['resultCode'] != NULL){ + if ($results['WsGetMembersResults']['results'][0]['resultMetadata']['resultCode'] == 'GROUP_NOT_FOUND'){ + return array(array("sourceId" => "NoAccess", "name" => "", "id" => "")); + } + } if (isset($results['WsGetMembersResults']['results'][0]['wsSubjects']) && $results['WsGetMembersResults']['results'][0]['wsSubjects'] != NULL) { return $results['WsGetMembersResults']['results'][0]['wsSubjects']; } } catch (Exception $e) { - CakeLog::write('error', __METHOD__ . ': An error occurred'); + CakeLog::write('error', __METHOD__ . ': An error occurred'); throw $e; } @@ -296,121 +309,173 @@ public function getMembersInGroup(array $queryData) { } /** - * Gets all Stems/Folders where User is admin/owner + * Add a member to a specific Grouper Group * * @param array $queryData Array of conditions for querying - * @return array Array of Stems/Folders from Grouper + * @return string Requests success or not * @throws GrouperLiteException */ - public function getOwnedStems(array $queryData) { - $queryData['groupType'] = 'StemOwner'; + public function addMemberToGroup(array $queryData) + { try { - $results = $this->useMembershipUrl($queryData); + $groupName = $queryData['groupName']; + + //Build request logic + $usersToAdd = array( + "WsRestAddMemberRequest" => array( + "subjectLookups" => array( + array("subjectId" => $queryData['addUserId']), + ), + "replaceAllExisting" => "F", + "actAsSubjectLookup" => array( + "subjectId" => $queryData['userId'] + ) + ) + ); - if (isset($results['WsGetMembershipsResults']['wsStems']) && $results['WsGetMembershipsResults']['wsStems'] != NULL) { - return $results['WsGetMembershipsResults']['wsStems']; + $this->http->setHeader(array('Content-Type' => 'application/json', 'Accept' => 'application/json')); + $connectionUrl = "{$this->config['fullUrl']}/groups/$groupName/members"; + + $results = $this->http->sendRequest('PUT', $connectionUrl, json_encode($usersToAdd)); + + // Parse out relevant records to send front end + if (isset($results['WsAddMemberResults']['results'][0]['resultMetadata']) && $results['WsAddMemberResults']['results'][0]['resultMetadata'] != NULL) { + return $results['WsAddMemberResults']['results'][0]['resultMetadata']['resultCode']; } } catch (Exception $e) { - CakeLog::write('error', __METHOD__ . ': An error occurred'); + CakeLog::write('error', __METHOD__ . ': An error occurred'); throw $e; } - return array(); + + return ""; } /** - * Used for requests made to Membership endpoint in Grouper WS - * - * @see getOptinGroups() - * @see getOwnedGroups() - * @see getOwnedStems() + * Remove a member from a specific Grouper Group * * @param array $queryData Array of conditions for querying - * @return array Group records associated to calling method + * @return string Requests success or not * @throws GrouperLiteException */ - private function useMembershipUrl(array $queryData) { - $groupType = $queryData['groupType']; - $userId = $queryData['userId']; - - if ($groupType == 'Optins') { - $fieldName = "optins"; - $subjectId = "GrouperAll"; - } elseif ($groupType == 'Owner') { - $fieldName = "admin"; - $subjectId = $userId; - } elseif ($groupType == 'StemOwner') { - $fieldName = "stemAdmin"; - $subjectId = $userId; - } else { - CakeLog::write('error', __METHOD__ . ": Option of $groupType is not supported"); - throw new GrouperLiteException("Option of $groupType is not supported"); - } + public function removeMemberToGroup(array $queryData) + { - //Build request logic - $groupsToShow = array( - "WsRestGetMembershipsRequest" => array( - "fieldName" => $fieldName, - "wsSubjectLookups" => array( - array("subjectId" => $subjectId) + try { + $groupName = $queryData['groupName']; + + //Build request logic + $userToDelete = array( + "WsRestDeleteMemberRequest" => array( + "subjectLookups" => array( + array("subjectId" => $queryData['remUserId']), + ), + "actAsSubjectLookup" => array( + "subjectId" => $queryData['userId'] + ) ) - ) - ); - $this->http->setHeader(array('Content-Type' => 'application/json', 'Accept' => 'application/json')); - $connectionUrl = "{$this->config['fullUrl']}/memberships"; + ); - try { - return $this->http->sendRequest('POST', $connectionUrl, json_encode($groupsToShow)); + $this->http->setHeader(array('Content-Type' => 'application/json', 'Accept' => 'application/json')); + $connectionUrl = "{$this->config['fullUrl']}/groups/$groupName/members"; + $results = $this->http->sendRequest('POST', $connectionUrl, json_encode($userToDelete)); + + // Parse out relevant records to send front end + if (isset($results['WsDeleteMemberResults']['results'][0]['resultMetadata']) && $results['WsDeleteMemberResults']['results'][0]['resultMetadata'] != NULL) { + return $results['WsDeleteMemberResults']['results'][0]['resultMetadata']['resultCode']; + } } catch (Exception $e) { - CakeLog::write('error', __METHOD__ . ': An error occurred'); + CakeLog::write('error', __METHOD__ . ': An error occurred'); throw $e; } + + return ""; } /** - * Returns a Distinct Grouper Group with its associated values + * Used for requests made to Membership endpoint in Grouper WS * * @param array $queryData Array of conditions for querying - * @return array Array of attributes for a Group + * @return array Group records associated to calling method * @throws GrouperLiteException + * + * @see getOwnedStems() + * @see getOptinGroups() + * @see getOptOutGroups() + * @see getOwnedGroups() */ - public function getGrouperGroupDescription(array $queryData) { - $groupName = $queryData['groupName']; + private function useMembershipUrl(array $queryData) + { + $groupType = $queryData['groupType']; + $userId = $queryData['userId']; + + if ($groupType == 'optins' || $groupType == 'optouts') { + $subjectId = "GrouperAll"; + } elseif ($groupType == 'admin' || $groupType == 'update' || $groupType == 'stemAdmin') { + $subjectId = $userId; + } else { + CakeLog::write('error', __METHOD__ . ": Option of $groupType is not supported"); + throw new GrouperLiteException("Option of $groupType is not supported"); + } - //Build request logic - $groupName = urlencode($groupName); - $connectionUrl = "{$this->config['fullUrl']}/groups/{$groupName}/memberships"; + if ($groupType == 'optins' || $groupType == 'optouts') { + //Build request logic, 2 subjectId's, second is for when user in "Secret" Optin/Optout Group + $groupsToShow = array( + "WsRestGetMembershipsRequest" => array( + "fieldName" => $groupType, + "wsSubjectLookups" => array( + array("subjectId" => $subjectId), + array("subjectId" => $userId) + ) + ) + ); + } else { + //Build request logic + $groupsToShow = array( + "WsRestGetMembershipsRequest" => array( + "fieldName" => $groupType, + "wsSubjectLookups" => array( + array("subjectId" => $subjectId) + ) + ) + ); + } - try { - $results = $this->http->sendRequest('GET', $connectionUrl); + $this->http->setHeader(array('Content-Type' => 'application/json', 'Accept' => 'application/json')); + $connectionUrl = "{$this->config['fullUrl']}/memberships"; - if (isset($results['WsGetMembershipsResults']['wsGroups']) && $results['WsGetMembershipsResults']['wsGroups'] != NULL) { - return $results['WsGetMembershipsResults']['wsGroups']; - } + try { + return $this->http->sendRequest('POST', $connectionUrl, json_encode($groupsToShow)); } catch (Exception $e) { - CakeLog::write('error', __METHOD__ . ': An error occurred'); + CakeLog::write('error', __METHOD__ . ': An error occurred'); throw $e; } - return array(); } /** - * Method used to CREATE a new Group in Grouper via the Template method. + * Method used to CREATE a new Group in Grouper via the Working Group Template method. * * @param array $queryData Array of conditions and data adding new Grouper Group - * @return bool True if added successfully + * @return array status and error message, if applicable * @throws GrouperLiteException * */ - public function createGroupWithTemplate(array $queryData) { - //Currently only supporting create group, need to test if update a group will work! + public function createGroupWithTemplate(array $queryData) + { + //Currently, only supporting create group, need to test if update a group will work! $data = $queryData['data']; $userId = $queryData['userId']; + //need to take out SympaDomain if domain not being created. + if ($data['gsh_input_isSympa'] == 'false') { + unset($data['gsh_input_sympaDomain']); + unset($data['gsh_input_isSympaModerated']); + } + //Build request logic $inputFields = array(); - foreach($data as $key => $value) { + foreach ($data as $key => $value) { $inputFields[] = array('name' => $key, 'value' => $value); } @@ -432,22 +497,46 @@ public function createGroupWithTemplate(array $queryData) { $this->http->setHeader(array('Content-Type' => 'application/json', 'Accept' => 'application/json')); $connectionUrl = "{$this->config['fullUrl']}/gshTemplateExec"; + $status = true; + $message = ''; try { $results = $this->http->sendRequest('POST', $connectionUrl, json_encode($groupToSave)); if (isset($results['WsGshTemplateExecResult']['resultMetadata']['resultCode'])) { - if (stripos($results['WsGshTemplateExecResult']['resultMetadata']['resultCode'], "SUCCESS", 0) !== false) { - return true; + if (stripos($results['WsGshTemplateExecResult']['resultMetadata']['resultCode'], "INVALID", 0) !== false) { + // Need to see what error message is + if (isset($results['WsGshTemplateExecResult']['gshValidationLines'])) { + //Just grab first one, since do not want to overload the user with errors, plus they won't understand message + $errorMessage = $results['WsGshTemplateExecResult']['gshValidationLines'][0]['validationText']; + $status = false; + if (stripos($errorMessage, 'already exist', 0) !== false) { + $message = 'There already is a WG named: ' . $data['gsh_input_workingGroupExtension']; + } else { + $message = 'An error occurred, please try again later.'; + } + } else { + $status = false; + $message = 'An error occurred, please try again later.'; + } + } elseif (stripos($results['WsGshTemplateExecResult']['resultMetadata']['resultCode'], "EXCEPTION", 0) !== false) { + throw new GrouperLiteException("An error occurred in creating your Working Group!"); } } } catch (Exception $e) { - CakeLog::write('error', __METHOD__ . ': An error occurred'); + CakeLog::write('error', __METHOD__ . ': An error occurred'); throw $e; } - return false; + + return array( + 'status' => $status, + 'message' => $message + ); + } /** + * ======================== NOT BEING USED ======================== + * * Method used to DELETE a Group in Grouper via the Template method. * * @param array $queryData Array of conditions and data adding new Grouper Group @@ -455,7 +544,8 @@ public function createGroupWithTemplate(array $queryData) { * @throws GrouperLiteException * */ - public function deleteGroupWithTemplate(array $queryData) { + public function deleteGroupWithTemplate(array $queryData) + { $workingGroupExt = $queryData['workingGroupExt']; $userId = $queryData['userId']; @@ -484,7 +574,7 @@ public function deleteGroupWithTemplate(array $queryData) { $connectionUrl = "{$this->config['fullUrl']}/gshTemplateExec"; try { - $results = $this->http->sendRequest('POST', $connectionUrl, json_encode($groupToSave)); + $results = $this->http->sendRequest('POST', $connectionUrl, json_encode($groupToDelete)); if (isset($results['WsGshTemplateExecResult']['resultMetadata']['resultCode'])) { if (stripos($results['WsGshTemplateExecResult']['resultMetadata']['resultCode'], "SUCCESS", 0) !== false) { @@ -492,13 +582,15 @@ public function deleteGroupWithTemplate(array $queryData) { } } } catch (Exception $e) { - CakeLog::write('error', __METHOD__ . ': An error occurred'); + CakeLog::write('error', __METHOD__ . ': An error occurred'); throw $e; } return false; } /** + * ======================== NOT BEING USED ======================== + * * For creating/updating Ad-Hoc groups not using the Grouper Template format * * Create or Update a Group where User is Admin/Owner @@ -507,8 +599,8 @@ public function deleteGroupWithTemplate(array $queryData) { * @return bool True if added or updated successful * @throws GrouperLiteException */ - public function createUpdateGroup(array $queryData) { - //TODO Currently only supporting create group, need to test if update a group will work! + public function createUpdateGroup(array $queryData) + { $groupName = htmlentities($queryData['name']); $stemName = htmlentities($queryData['stem']); @@ -554,22 +646,26 @@ public function createUpdateGroup(array $queryData) { } } } catch (Exception $e) { - CakeLog::write('error', __METHOD__ . ': An error occurred'); + CakeLog::write('error', __METHOD__ . ': An error occurred'); throw $e; } return false; } /** - * Listing of attributes in Grouper for a given Group Name + * ======================== NOT BEING USED ======================== + * Used to be used for Grouper group info page. + * + * Grouper Group information plus a listing of attributes in Grouper for that given Group * * @param array $queryData Array of conditions for querying * @return array Record of Grouper attributes for given GroupName * @throws GrouperLiteException */ - public function getGroupAttributes(array $queryData) { - // Need to pass in the full stem path, so thing like sandbox:app:sympa are good! + public function getGrouperGroupInfo(array $queryData) + { $groupName = $queryData['groupName']; + $groupInfo = array(); //Build request logic $stemToFind = array( @@ -589,11 +685,48 @@ public function getGroupAttributes(array $queryData) { try { $results = $this->http->sendRequest('POST', $connectionUrl, json_encode($stemToFind)); + //Get the group information + if (isset($results['WsGetAttributeAssignmentsResults']['wsGroups']) && $results['WsGetAttributeAssignmentsResults']['wsGroups'] != NULL) { + $groupInfo = $results['WsGetAttributeAssignmentsResults']['wsGroups']; + } + + //Now get the Group Attributes and add them to group if (isset($results['WsGetAttributeAssignmentsResults']['wsAttributeAssigns']) && $results['WsGetAttributeAssignmentsResults']['wsAttributeAssigns'] != NULL) { - return $results['WsGetAttributeAssignmentsResults']['wsAttributeAssigns']; + $groupInfo[0]["attributes"] = $results['WsGetAttributeAssignmentsResults']['wsAttributeAssigns']; + } else { + $groupInfo[0]["attributes"] = array(); } + + return $groupInfo; } catch (Exception $e) { - CakeLog::write('error', __METHOD__ . ': An error occurred'); + CakeLog::write('error', __METHOD__ . ': An error occurred'); + throw $e; + } + } + + /** + * ======================== NOT BEING USED ======================== + * + * Potential use was for creating adhoc group by a user, not associated to WG. + * + * Gets all Stems/Folders where User is admin/owner + * + * @param array $queryData Array of conditions for querying + * @return array Array of Stems/Folders from Grouper + * @throws GrouperLiteException + */ + public function getOwnedStems(array $queryData) + { + $queryData['groupType'] = 'stemAdmin'; + + try { + $results = $this->useMembershipUrl($queryData); + + if (isset($results['WsGetMembershipsResults']['wsStems']) && $results['WsGetMembershipsResults']['wsStems'] != NULL) { + return $results['WsGetMembershipsResults']['wsStems']; + } + } catch (Exception $e) { + CakeLog::write('error', __METHOD__ . ': An error occurred'); throw $e; } return array(); diff --git a/Lib/GrouperHTTPWrapper.php b/Lib/GrouperHTTPWrapper.php index 625d96c..dbcd822 100644 --- a/Lib/GrouperHTTPWrapper.php +++ b/Lib/GrouperHTTPWrapper.php @@ -113,39 +113,17 @@ public function sendRequest(string $method, string $uri, string $body = ''): arr $this->_request['body'] = $body; try { - $results = $this->request($this->_request); + $apiResults = $this->request($this->_request); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': An error occurred: ' . var_export($e->getMessage(), true)); throw new GrouperLiteException('An error occurred talking to Grouper WS'); } - return $this->_verifyResults($results); - } - /** - * Verify data from Grouper Web Service came back successfully, if not log error and send to front end. - * - * @param HttpSocketResponse $apiResults Results from Grouper WS - * @return array array of records | array with error message - * @throws GrouperLiteException If issue with Grouper WS data returned - */ - private function _verifyResults(HttpSocketResponse $apiResults): array { - - $resBody = array(); - if ($apiResults->isOk()) { - $resBody = json_decode($apiResults->body(), true); - - $mainKey = key($resBody); - $apiSuccess = $resBody[$mainKey]['resultMetadata']['resultCode']; - - if ($apiSuccess != 'SUCCESS') { - CakeLog::write('error', __METHOD__ . ': Result Code was ' . var_export($apiSuccess, true)); - CakeLog::write('error', __METHOD__ . ': Error of ' . var_export($apiResults->body(), true)); - throw new GrouperLiteException('Result from Grouper WS was' . var_export($apiSuccess, true)); - } - } else { - CakeLog::write('error', __METHOD__ . ': Verify error of ' . var_export($apiResults->body(), true)); - throw new GrouperLiteException('An error occurred while talking to Grouper WS'); + // Call may return non-200, which may be okay depending on call + if (!$apiResults->isOk()) { + CakeLog::write('error', __METHOD__ . ': Grouper WS returned non-200 of ' . var_export($apiResults->body(), true)); } - return $resBody; + + return json_decode($apiResults->body(), true); } } diff --git a/Lib/lang.php b/Lib/lang.php index 80f2baf..bcae1dd 100644 --- a/Lib/lang.php +++ b/Lib/lang.php @@ -3,10 +3,18 @@ $cm_grouper_lite_texts['en_US'] = array( 'pl.grouperlite.config.display.title' => 'Grouper Configuration Settings', 'pl.grouperlite.config.edit.title' => 'Edit Grouper Configuration Settings', - 'pl.grouperlite.config.grouper-url' => 'Grouper URL', - 'pl.grouperlite.config.grouper-version' => 'Grouper Version', - 'pl.grouperlite.config.grouper-un' => 'Username', - 'pl.grouperlite.config.grouper-pw' => 'Password', + 'pl.grouperlite.config.grouper-url' => 'Grouper Site URL', + 'pl.grouperlite.config.grouper-url-subscript' => '(Example: https://grouper.prod.at.internet2.edu)', + 'pl.grouperlite.config.grouper-ws-url' => 'Grouper Webservice URL', + 'pl.grouperlite.config.grouper-ws-version' => 'Grouper Webservice Version', + 'pl.grouperlite.config.grouper-ws-un' => 'Webservice Username', + 'pl.grouperlite.config.grouper-ws-pw' => 'Webservice Password', + 'pl.grouperlite.config.ad-hoc-heading' => 'Ad-hoc Group Heading', + 'pl.grouperlite.config.ad-hoc-subscript' => '', + 'pl.grouperlite.config.wg-heading' => 'Working Group Heading', + 'pl.grouperlite.config.wg-subscript' => '', + 'pl.grouperlite.config.default-collapse' => 'Collapse groups by default?', + 'pl.grouperlite.config.default-collapse-subscript' => '', 'pl.grouperlite.crumb.root' => 'Grouper', 'pl.grouperlite.nav.groups-can-join' => 'Groups I can join', @@ -16,10 +24,14 @@ 'pl.grouperlite.nav.emaillists-member' => 'My email lists', 'pl.grouperlite.nav.emaillists-manage' => 'Email lists I manage', 'pl.grouperlite.nav.create-group' => 'Create group', + 'pl.grouperlite.nav.create-working-group' => 'Create working group', 'pl.grouperlite.nav.create-email' => 'Create email list', 'pl.grouperlite.nav.emaillists' => 'Email Lists', 'pl.grouperlite.nav.groups' => 'Groups', + 'pl.grouperlite.dashboard.heading.groups' => 'Groups', + 'pl.grouperlite.dashboard.heading.email-lists' => 'Email lists', + 'pl.grouperlite.title.root' => 'Grouper Collaborations:', 'pl.grouperlite.title.groupinfo' => 'Group configuration and attributes', 'pl.grouperlite.title.groupowner' => 'Groups I manage', @@ -29,16 +41,16 @@ 'pl.grouperlite.title.emaillists-member' => 'My email lists', 'pl.grouperlite.title.emaillists-manage' => 'Email lists I manage', 'pl.grouperlite.title.emaillistsinfo' => 'Email list configuration and attributes', - 'pl.grouperlite.title.groupcreate' => 'Create Group', - 'pl.grouperlite.title.templatecreate' => 'Create a Templated Group', + 'pl.grouperlite.title.groupcreate' => 'Create a group', + 'pl.grouperlite.title.templatecreate' => 'Create a working group', - 'pl.grouperlite.message.flash.join-group-success' => 'You have been added to the group!', - 'pl.grouperlite.message.flash.join-group-failed' => 'You are unable to join the group!', + 'pl.grouperlite.message.flash.join-group-success' => 'You have been added to the group:', + 'pl.grouperlite.message.flash.join-group-failed' => 'You are unable to join the group:', 'pl.grouperlite.message.flash.join-group-error' => 'An error occurred in joining the group, please try again later.', 'pl.grouperlite.message.flash.info-group-failed' => 'Error in viewing group info, please try again later.', - 'pl.grouperlite.message.flash.leave-group-success' => 'You have been deleted from the group!', - 'pl.grouperlite.message.flash.leave-group-failed' => 'You are unable to delete the group!', + 'pl.grouperlite.message.flash.leave-group-success' => 'You have been deleted from the group:', + 'pl.grouperlite.message.flash.leave-group-failed' => 'You are unable to be removed from the group:', 'pl.grouperlite.message.flash.leave-group-error' => 'An error occurred in leaving the group, please try again later.', 'pl.grouperlite.message.flash.owner-group-failed' => 'Error occurred in viewing groups you manage, please try again later.', 'pl.grouperlite.message.flash.member-group-failed' => 'Error occurred in viewing groups you are a member, please try again later.', @@ -46,8 +58,14 @@ 'pl.grouperlite.message.flash.group-detail-members-failed' => 'Error in viewing the members of this group, please try again later.', + 'pl.grouperlite.message.flash.add-subscriber-success' => 'has been added to the group:', + 'pl.grouperlite.message.flash.add-subscriber-failed' => 'Error in adding subscriber.', + 'pl.grouperlite.message.flash.remove-subscriber-success' => 'has been removed from the group:', + 'pl.grouperlite.message.flash.remove-subscriber-failed' => 'Error in removing subscriber.', + 'pl.grouperlite.table.name' => 'Name', 'pl.grouperlite.table.description' => 'Description', + 'pl.grouperlite.table.role' => 'Role', 'pl.grouperlite.table.status' => 'Status', 'pl.grouperlite.table.action' => 'Action', 'pl.grouperlite.table.open' => 'Access', @@ -79,19 +97,33 @@ 'pl.grouperlite.value.enabled' => 'Enabled', 'pl.grouperlite.value.disabled' => 'Disabled', + 'pl.grouperlite.value.email' => 'Email address of Working Group List', + 'pl.grouperlite.value.jira' => 'For this Group, Jira provides a place to plan, track, and manage your agile and software development projects.', + 'pl.grouperlite.value.confluence' => 'The Confluence group is a team workspace, specifically for this Group, where knowledge and collaboration meet allowing for the ability to Create, collaborate, and organize content in one place.', + 'pl.grouperlite.value.incommon-collab' => '', + 'pl.grouperlite.action.join' => 'Join', 'pl.grouperlite.action.leave' => 'Leave', 'pl.grouperlite.action.edit-group' => 'Edit', 'pl.grouperlite.action.disable-group' => 'Disable', 'pl.grouperlite.action.subscribe' => 'Subscribe', 'pl.grouperlite.action.unsubscribe' => 'Unsubscribe', - 'pl.grouperlite.action.search' => 'Search', + 'pl.grouperlite.action.search' => 'Search For Groups', 'pl.grouperlite.action.remove' => 'Remove', 'pl.grouperlite.action.view' => 'View', 'pl.grouperlite.action.edit' => 'Edit', 'pl.grouperlite.action.view-in-grouper' => 'View in Grouper', 'pl.grouperlite.action.view-members' => 'View members', + 'pl.grouperlite.action.grouper' => 'Grouper', + 'pl.grouperlite.action.members' => 'Members', 'pl.grouperlite.action.close' => 'Close', + 'pl.grouperlite.action.clear' => 'Clear', + 'pl.grouperlite.action.add-user' => 'Add', + 'pl.grouperlite.action.remove-user' => 'Remove', + 'pl.grouperlite.message.user-not-found-error' => 'Error: User not found.', + 'pl.grouperlite.message.user-not-added-error' => 'Error: Unable to add user.', + 'pl.grouperlite.message.user-not-removed-error' => 'Error: Unable to remove user.', + 'pl.grouperlite.message.user-authorization-error' => 'Error: Not authorized to add user.', 'pl.grouperlite.form.group.template.label' => 'Select a template', 'pl.grouperlite.form.group.template.empty' => '(choose one)', @@ -120,6 +152,7 @@ 'pl.grouperlite.form.template.value.internet2' => 'Internet2', 'pl.grouperlite.form.template.value.incommon' => 'Incommon', 'pl.grouperlite.form.template.is-optin.label' => 'Is optin?', + 'pl.grouperlite.form.template.is-moderated.label' => 'Is moderated?', 'pl.grouperlite.form.template.add-wiki.label' => 'Add wiki?', 'pl.grouperlite.form.template.add-project.label' => 'Add project?', 'pl.grouperlite.form.template.value.positive' => 'Yes', @@ -127,5 +160,11 @@ 'pl.grouperlite.search.tags.text' => 'Search', - 'pl.grouperlite.pagination.counter' => 'Viewing {:start}-{:end} of {:count}' + 'pl.grouperlite.pagination.counter' => 'Viewing {:start}-{:end} of {:count}', + 'pl.grouperlite.attributes.zero-state' => 'No Attributes Associated to this Group.', + 'pl.grouperlite.groups.zero-state' => 'No groups found.', + 'pl.grouperlite.working-groups.zero-state' => 'None.', + 'pl.grouperlite.email-lists.zero-state' => 'No email lists found.', + 'pl.grouperlite.members.noaccess' => 'You do not have access to view memberships.', + 'pl.grouperlite.members.empty' => 'This group has no member OR you are not authorized to see the members of this group.' ); \ No newline at end of file diff --git a/Model/CoGrouperLite.php b/Model/CoGrouperLite.php index a0798e3..1226065 100644 --- a/Model/CoGrouperLite.php +++ b/Model/CoGrouperLite.php @@ -55,21 +55,44 @@ class CoGrouperLite extends CoDashboardWidgetBackend { //Tried adding rule to each field to make alphaNumeric, but keeps throwing errors. will research. public $validate = array( 'co_dashboard_widget_id' => array( - 'rule' => 'numeric', + 'rule' => 'alphaNumeric', 'required' => true ), - 'connUrl' => array( + 'conn_url' => array( + 'rule' => array('custom', '/^https?:\/\/.*/'), 'required' => true ), - 'connVer' => array( + 'conn_ver' => array( + 'rule' => array('minLength', 4), 'required' => true ), - 'connUser' => array( + 'grouper_url' => array( + 'rule' => array('custom', '/^https?:\/\/.*/'), 'required' => true ), - 'connPass' => array( + 'conn_user' => array( + 'rule' => array('minLength', 1), 'required' => true ), + 'conn_pass' => array( + 'rule' => array('minLength', 1), + 'required' => true + ), + 'adhoc_heading' => array( + 'rule' => array('minLength', 1), + 'required' => false, + 'default' => 'Ad-hoc groups' + ), + 'wg_heading' => array( + 'rule' => array('minLength', 1), + 'required' => false, + 'default' => 'Working groups' + ), + 'default_collapse' => array( + 'rule' => array('minLength', 1), + 'required' => false, + 'default' => 'collapsed' + ), ); } \ No newline at end of file diff --git a/Model/GrouperGroup.php b/Model/GrouperGroup.php index 89bf151..9bff932 100644 --- a/Model/GrouperGroup.php +++ b/Model/GrouperGroup.php @@ -44,9 +44,30 @@ class GrouperGroup extends GrouperLiteAppModel private $totalRecords = 0; - /** @var string Group whose members can create Groups via Template process*/ + /** @var string Group whose members can create Groups via Template process */ private $templateCreationGroup = 'ref:workinggroupadmins'; + /** @var string Group whose members cannot see Grouper button, even if admin */ + private $grouperVisibleGroup = 'app:comanage:LiteUI:grouperVisible'; + + private $wgStemsTopLevel = array( + 'ref:incommon-collab', + 'ref:internet2-collab' + ); + + // Current listing of groups that are associated to Working Groups + private $wgStemsAllGroups = array( + 'app:jira', + 'app:confluence', + 'app:sympa:internet2', + 'app:sympa:incommon', + 'ref:incommon-collab', + 'ref:internet2-collab' + ); + + //Stem for email groups, only email groups using this stem are viewable + private $emailStem = 'app:sympa'; + /** * Verifies if user is an owner/admin of a group and then stores results in Session. @@ -59,7 +80,8 @@ class GrouperGroup extends GrouperLiteAppModel * @see GrouperGroup::resetUserOwner() * */ - public function isUserOwner(string $userId) { + public function isUserOwner(string $userId) + { if (CakeSession::check('Plugin.Grouper.isUserOwner')) { return CakeSession::read('Plugin.Grouper.isUserOwner'); } @@ -85,6 +107,52 @@ public function isUserOwner(string $userId) { } } + /** + * Verifies if user can see the Grouper link button. Even if owner, may be not allowed to see it. + * + * @param string $userId Id of User + * @return String T or F + * @throws GrouperLiteException + * + */ + public function isGrouperVisible(string $userId) + { + if (CakeSession::check('Plugin.Grouper.isGrouperVisible')) { + return CakeSession::read('Plugin.Grouper.isGrouperVisible'); + } + + $this->initApi(); + + try { + $args = array(); + $args['userId'] = $userId; + + $memberOfGroups = $this->grouperAPI->getGrouperMemberOfGroups($args); + + $memberOfGroups = $this->grouperAPI->getGrouperMemberOfGroups($args); + + //now cycle through and see if part of correct group to be able to use template + $member = 'F'; + foreach ($memberOfGroups as $memberOfGroup) { + if ($memberOfGroup['name'] == $this->grouperVisibleGroup) { + $member = 'T'; + break; + } + } + + if ($member == 'T') { + CakeSession::write('Plugin.Grouper.isGrouperVisible', 'T'); + return 'T'; + } else { + CakeSession::write('Plugin.Grouper.isGrouperVisible', 'F'); + return 'F'; + } + + } catch (Exception $e) { + CakeLog::write('error', __METHOD__ . ': An error occurred'); + throw $e; + } + } /** * Used to instantiate API class @@ -94,7 +162,8 @@ public function isUserOwner(string $userId) { * so error was being thrown. * Now will call this before each function call to verify set. */ - private function initApi() { + private function initApi() + { if ($this->grouperAPI == null) { $this->grouperAPI = new GrouperApiAccess(); } @@ -102,35 +171,40 @@ private function initApi() { /** * Return all Groups that a User belongs to in Grouper. - * Will also add Optin Groups and flag them as joined so can display Optout option in UI. + * Will also add OptOut Groups and flag them as joined so can display Optout option in UI. * * @param array $conditions Listing of conditions for display of records, including UserId * @return array Records of Groups from Grouper that the User belongs to * @throws GrouperLiteException * */ - public function filteredMemberOfGroups(array $conditions) { + public function filteredMemberOfGroups(array $conditions) + { $this->initApi(); try { + $memberOfGroups = $this->memberOfGroups($conditions); + $conditions['groupType'] = 'optouts'; // Determine which groups can be left by user, if want. - $optInGroups = $this->grouperAPI->getOptinGroups($conditions); + $optOutGroups = $this->grouperAPI->getOptionalGroups($conditions); foreach ($memberOfGroups as &$memberOfGroup) { - foreach ($optInGroups as $key => $value) { + $memberOfGroup['optOut'] = false; + foreach ($optOutGroups as $key => $value) { if ($value['name'] == $memberOfGroup['name']) { //Match! - $memberOfGroup['optedin'] = true; + $memberOfGroup['optOut'] = true; //Remove Optin group since already found and now less loops - unset($optInGroups[$key]); + unset($optOutGroups[$key]); break; } } } - return $this->getFriendlyName($memberOfGroups); + $groupResults = array_values($memberOfGroups); + return $this->getFriendlyWorkingGroupName($groupResults, 'member'); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': An error occurred'); @@ -138,19 +212,6 @@ public function filteredMemberOfGroups(array $conditions) { } } - public function filteredMemberOfEmails(array $conditions) { - $memberOfEmails = $this->filteredMemberOfGroups($conditions); - - // Strip out all Groups that are not in Sympa Stem/Directory - foreach($memberOfEmails as $key => $value){ - if(strpos($value['name'], 'sympa') === false) { - unset($memberOfEmails[$key]); - } - } - - return $memberOfEmails; - } - /** * Internal process used by other functions to fetch Groups the User is a member of * @@ -159,7 +220,8 @@ public function filteredMemberOfEmails(array $conditions) { * @throws GrouperLiteException * */ - private function memberOfGroups(array $conditions) { + private function memberOfGroups(array $conditions) + { try { return $this->grouperAPI->getGrouperMemberOfGroups($conditions); @@ -170,99 +232,76 @@ private function memberOfGroups(array $conditions) { } } + /** - * Determine if result set contains friendly name, if so add as a new attribute in result set + * Process for User to Leave a Group * - * @param array $groups Current result set of Groups - * @return array Same result set with added param for friendly name + * @param string $userId Id of User + * @param string $groupName Name of Group Leaving, do not confuse with DisplayName field! + * @return bool True|False + * @throws GrouperLiteException * */ - private function getFriendlyName(array $groups) { + public function leaveGroup(string $userId, string $groupName) + { + $this->initApi(); - //NOT fetching attributes now, just using name display name as friendly name, if different from name field - foreach ($groups as &$group) { - //$group['friendlyName'] = $group['displayName']; - $nameSections = explode(':', $group['name']); - $friendlySections = explode(':', $group['displayName']); - $numberNameSections = count($nameSections); - for ($x = 0; $x < $numberNameSections; $x++) { - if ($nameSections[$x] == $friendlySections[$x]) { - unset($friendlySections[$x]); - } - } - $group['friendlyName'] = implode(':', $friendlySections); - if (strlen($group['friendlyName']) == 0) { - $group['friendlyName'] = $group['name']; - } - } + try { + $args = array(); + $args['LeaveJoin'] = 'Leave'; + $args['userId'] = $userId; + $args['groupName'] = $groupName; - return $groups; + return $this->grouperAPI->grouperGroupLeaveOrJoin($args); - /* Old Process, keeping in case changes back! - foreach ($groups as &$group) { - $group['friendlyName'] = $group['displayName']; - $attributes = $this->grouperAPI->getGroupAttributes(array('conditions' => array('groupName' => $group['displayName']))); - foreach ($attributes as $attribute) { - if ($attribute['attributeDefNameName'] == $this->friendly) { - if (isset($attribute['wsAttributeAssignValues']['valueSystem'])) { - $group['friendlyName'] = $attribute['wsAttributeAssignValues']['valueSystem']; - } - break; - } - } - }*/ + } catch (Exception $e) { + CakeLog::write('error', __METHOD__ . ': An error occurred'); + throw $e; + } } /** - * Gets the Grouper Groups params and values as well as its associated attributes. + * Process for User to Join a Group * - * @param string $groupName Name of Group, do not confuse with DisplayName field! - * @return array An set of attributes associated to a specific Grouper Group + * @param string $userId Id of User + * @param string $groupName Name of Group Joining, do not confuse with DisplayName field! + * @return bool True|False * @throws GrouperLiteException + * */ - public function groupDescriptions(string $groupName) { + public function joinGroup(string $userId, string $groupName) + { $this->initApi(); try { $args = array(); + $args['LeaveJoin'] = 'Join'; + $args['userId'] = $userId; $args['groupName'] = $groupName; - $groupDescription = $this->grouperAPI->getGrouperGroupDescription($args); - $groupInfo = $this->getFriendlyName($groupDescription); - - //make call to get attributes for group - $grouperAtt = new GrouperAttribute(); - $groupAttributes = $grouperAtt->getGroupAttributes($groupName); - - $groupInfo[0]["attributes"] = $groupAttributes; - return $groupInfo; + return $this->grouperAPI->grouperGroupLeaveOrJoin($args); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': An error occurred'); throw $e; } - } /** - * Process for User to Leave a Group + * Return all Grouper Groups that the User has a role of owner/admin * - * @param string $userId Id of User - * @param string $groupName Name of Group Leaving, do not confuse with DisplayName field! - * @return bool True|False + * @param array $conditions Listing of conditions for display of records, including UserId + * @return array * @throws GrouperLiteException * */ - public function leaveGroup(string $userId, string $groupName) { + public function ownerGroups(array $conditions) + { $this->initApi(); try { - $args = array(); - $args['LeaveJoin'] = 'Leave'; - $args['userId'] = $userId; - $args['groupName'] = $groupName; - - return $this->grouperAPI->grouperGroupLeaveOrJoin($args); + $resultSet = $this->grouperAPI->getOwnedGroups($conditions); + return $this->getFriendlyName(array_values($resultSet)); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': An error occurred'); @@ -271,24 +310,37 @@ public function leaveGroup(string $userId, string $groupName) { } /** - * Process for User to Join a Group + * Get members associated to a specific Grouper Group + * NOTE: This list only shows members, it does not pull in other groups that may be attached in Grouper as + * members * + * @param array $conditions Listing of conditions for display of records * @param string $userId Id of User - * @param string $groupName Name of Group Joining, do not confuse with DisplayName field! - * @return bool True|False - * @throws GrouperLiteException + * @return array Listing of members in requested Grouper Group + * @throws GrouperLiteException Captured in Controller * */ - public function joinGroup(string $userId, string $groupName) { + public function membersInGroup(array $conditions, string $userId) + { $this->initApi(); + $conditions['userId'] = $userId; + try { - $args = array(); - $args['LeaveJoin'] = 'Join'; - $args['userId'] = $userId; - $args['groupName'] = $groupName; + $groupMembers = $this->grouperAPI->getMembersInGroup($conditions); - return $this->grouperAPI->grouperGroupLeaveOrJoin($args); + if (count($groupMembers) < 1) { + return $groupMembers; + } + + $finalMembers = array(); + foreach ($groupMembers as $member) { + if ($member['sourceId'] !== 'g:gsa') { + $finalMembers[] = $member; + } + + } + return $finalMembers; } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': An error occurred'); @@ -297,20 +349,24 @@ public function joinGroup(string $userId, string $groupName) { } /** - * Return all Grouper Groups that the User has a role of owner/admin + * Add a member to a specific Grouper Group * - * @param array $conditions Listing of conditions for display of records, including UserId - * @return array - * @throws GrouperLiteException + * @param array $conditions Listing of conditions for display of records + * @param string $userId Id of User + * @return string success of Request + * @throws GrouperLiteException Captured in Controller * */ - public function ownerGroups(array $conditions) { + public function addMemberToGroup(array $conditions, string $userId) + { $this->initApi(); + $conditions['userId'] = $userId; + try { - $ownGroups = $this->grouperAPI->getOwnedGroups($conditions); + $resultAdd = $this->grouperAPI->addMemberToGroup($conditions); - return $this->getFriendlyName($ownGroups); + return $resultAdd; } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': An error occurred'); @@ -319,18 +375,24 @@ public function ownerGroups(array $conditions) { } /** - * Get members associated to a specific Grouper Group + * Remove a member from a specific Grouper Group * * @param array $conditions Listing of conditions for display of records - * @return array Listing of members in requested Grouper Group + * @param string $userId Id of User + * @return string success of Request * @throws GrouperLiteException Captured in Controller * */ - public function membersInGroup(array $conditions) { + public function removeMemberToGroup(array $conditions, string $userId) + { $this->initApi(); + $conditions['userId'] = $userId; + try { - return $this->grouperAPI->getMembersInGroup($conditions); + $resultRemove = $this->grouperAPI->removeMemberToGroup($conditions); + + return $resultRemove; } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': An error occurred'); @@ -338,6 +400,7 @@ public function membersInGroup(array $conditions) { } } + /** * Return all Groups the User can JOIN * Get all Groups with Optin attribute set and display ones User can join. @@ -347,11 +410,14 @@ public function membersInGroup(array $conditions) { * @return array Listing of Optin groups available in Grouper * @throws GrouperLiteException Captured in Controller */ - public function optinGroups(array $conditions) { + public function optinGroups(array $conditions) + { $this->initApi(); try { - $joinOrLeave = $this->grouperAPI->getOptinGroups($conditions); + $conditions['groupType'] = 'optins'; + + $joinOrLeave = $this->grouperAPI->getOptionalGroups($conditions); $userGroups = $this->memberOfGroups($conditions); //See if Optin group match any of the groups user already belongs to. @@ -365,9 +431,8 @@ public function optinGroups(array $conditions) { } } } - //$finalSet = $this->paginateRecords($joinOrLeave, $conditions); - return $this->getFriendlyName($joinOrLeave); + return $this->getFriendlyName(array_values($joinOrLeave)); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': An error occurred'); @@ -375,27 +440,15 @@ public function optinGroups(array $conditions) { } } - public function optinEmailGroups(array $conditions) { - $allGroups = $this->optinGroups($conditions); - - // Strip out all Groups that are not in Sympa Stem/Directory - foreach($allGroups as $key => $value){ - if(strpos($value['name'], 'sympa') === false) { - unset($allGroups[$key]); - } - } - - return $allGroups; - } - /** - * Determine if User can use the Grouper Template to create a suite of Groups including Email lists. + * Determine if User can use the Grouper Template to create a Working Group. * * @param string $userId Id of User * @return string T for True and F for False * @throws GrouperLiteException */ - public function isTemplateUser(string $userId) { + public function isTemplateUser(string $userId) + { if (CakeSession::check('Plugin.Grouper.isTemplateUser')) { return CakeSession::read('Plugin.Grouper.isTemplateUser'); } @@ -431,17 +484,17 @@ public function isTemplateUser(string $userId) { } - /** * Create a new Grouper Group using the Template methodology in Grouper * * @param string $userId Id of User * @param array $groupData Data needed to create new Grouper Group via Template - * @return bool True if successfully created record + * @return array status and error message, if applicable * @throws GrouperLiteException * */ - public function createGroupWithTemplate(string $userId, array $groupData) { + public function createGroupWithTemplate(string $userId, array $groupData) + { $this->initApi(); try { @@ -454,7 +507,7 @@ public function createGroupWithTemplate(string $userId, array $groupData) { 'gsh_input_isJira' ); // Template does not except true/false, so convert to string and send that way - foreach($fields as $field) { + foreach ($fields as $field) { ($groupData[$field] == '0') ? $groupData[$field] = 'false' : $groupData[$field] = 'true'; } @@ -474,98 +527,22 @@ public function createGroupWithTemplate(string $userId, array $groupData) { } - /** - * Delete a Grouper Group using the Template methodology in Grouper - * - * @param string $userId Id of User - * @param array $groupData Data needed to delete a Grouper Group via Template - * @return bool True if successfully deleted record - * @throws GrouperLiteException - * - */ - public function deleteGroupWithTemplate(string $userId, array $groupData) { - $this->initApi(); - - try { - $args = array(); - $args['userId'] = $userId; - $args['workingGroupExt'] = $groupData['workingGroupExt']; - - // Reset Session variable that shows if User is an owner or not - $this->resetUserOwner(); - - return $this->grouperAPI->deleteGroupWithTemplate($args); - - } catch (Exception $e) { - CakeLog::write('error', __METHOD__ . ': An error occurred'); - throw $e; - } - - } - - /** - * Creates/Updates a new Grouper Group not Template related - * - * @param string $userId Id of user - * @param array $data Data from form to Save in Grouper as a Group - * @return bool True if saved | False if already created - * @throws GrouperLiteException - * - */ - public function createUpdateGroup(string $userId, array $data) { - $this->initApi(); - - try { - $args = $data; - $args['userId'] = $userId; - - // Reset Session variable that shows if User is an owner or not - $this->resetUserOwner(); - - return $this->grouperAPI->createUpdateGroup($args); - - } catch (Exception $e) { - CakeLog::write('error', __METHOD__ . ': An error occurred'); - throw $e; - } - } - /** * Private function used to reset the IsUserOwner Session variable, * that maintains the status of a user being an owner/admin of a group * * @see GrouperGroup::isUserOwner() */ - private function resetUserOwner() { + private function resetUserOwner() + { if (CakeSession::check('Plugin.Grouper.isUserOwner')) { CakeSession::delete('Plugin.Grouper.isUserOwner'); } } - /** - * Get Grouper Stems where User has authority to create a Grouper Group - * - * @param string $userId Id of User - * @return array Stem records - * @throws GrouperLiteException - */ - public function getOwnedStems(string $userId) { - $this->initApi(); - - try { - $args = array(); - $args['userId'] = $userId; - - return $this->grouperAPI->getOwnedStems($args); - - } catch (Exception $e) { - CakeLog::write('error', __METHOD__ . ': An error occurred'); - throw $e; - } - } - /** * Search for Groups/Lists related to Search term. + * * Will import all records the user can see and then do search in this code rather than call Grouper WS Search * functionality. This is due to the fact that the grouperName is autogenerated and this app needs to search * attributes which the Grouper WS does not do. @@ -574,7 +551,8 @@ public function getOwnedStems(string $userId) { * @return array Records that meet search criteria * @throws Exception Captured in Controller */ - public function getSearchedGroups(array $conditions) { + public function getSearchedGroups(array $conditions) + { $this->initApi(); try { @@ -586,13 +564,23 @@ public function getSearchedGroups(array $conditions) { $searchCriteria = $conditions['searchcriteria']; foreach ($pageResults as $result) { - $match = preg_grep("/$searchCriteria/i", $result); + $compare = $result; + unset($compare['extension']); + unset($compare['uuid']); + unset($compare['enabled']); + unset($compare['typeOfGroup']); + unset($compare['idIndex']); + $match = preg_grep("/$searchCriteria/i", $compare); if (!empty($match)) { $returnResults[] = $result; } } - return $returnResults; + if(isset($conditions['']) && $conditions['getSearchedGroups']){ + return $this->getFriendlyWorkingGroupName($returnResults, 'member'); + } else { + return $returnResults; + } } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': An error occurred'); @@ -601,65 +589,165 @@ public function getSearchedGroups(array $conditions) { } /** - * Method needed to support Pagination - * Adjusted to fit the data source being the Grouper API vs DB calls + * Return array of Working Groups for display on coManage site. + * Logic is for each WG to have one key=>value of main WG name, then array of all associated + * Groups. + * + * NOTE: This is a major hack due to Grouper not giving us the right logic for displaying, so have to run all + * groups through a mapped listing of types of Groups in a WG to see if match and then parse and massage to display * - * @param array $conditions Listing of conditions for display of records with pagination - * @return array Records requested by user with pagination support + * @param array $groups Listing of Groups + * @return array Listing of Groups in WG format for display * */ - public function paginate($conditions) { + private function getFriendlyWorkingGroupName(array $groups, $method) + { + $arrayIndex = 0; + $workingGroups = array(); + + //First need to loop through all groups and pull in all top levels + $topLevelWG = array(); + foreach ($groups as $group) { + foreach ($this->wgStemsTopLevel as $stem) { + $len = strlen($stem); + if (substr(strtolower($group['name']), 0, $len) === $stem) { + $stemSections = explode(':', $group['name']); + //Get third section, since will always by after ref:something:here + if (in_array($stemSections[2], $topLevelWG) === false) { + $topLevelWG[] = $stemSections[2]; + } + } + } + } - //Pull out the method that should be run. - $method = $conditions['method']; + //Loop through groups to see if possibly part of a Working Group + foreach ($groups as &$group) { + foreach ($this->wgStemsAllGroups as $stem) { + $len = strlen($stem); + // if match to name of group within WG mapping then start making a WG group array + if (substr(strtolower($group['name']), 0, $len) === $stem) { + $tempGroup = $group; + if ($stem == 'ref:incommon-collab' || $stem == 'ref:internet2-collab') { + $mainGroup = true; + } else { + $mainGroup = false; + } + $stemSections = explode(':', $group['name']); + $displaySections = explode(':', $group['displayName']); + //Get second to last stem section + $sectionCount = 2; + //If group not part of a top level WG, then do not show! + if (in_array($stemSections[$sectionCount], $topLevelWG) === false) { + break; + } + $tempGroup['WGName'] = $stemSections[$sectionCount]; + $tempGroup['WGShowName'] = $displaySections[$sectionCount]; + // Get user type, which is after the WG name + if (isset($stemSections[$sectionCount + 1])) { + $tempGroup['WGRole'] = $stemSections[$sectionCount + 1]; + } else { + $tempGroup['WGRole'] = ''; + } + $appCount = 0; + $appName = ''; + foreach ($stemSections as $stemSection) { + //Skip first entry + if ($appCount > 0) { + if ($appCount < $sectionCount) { + if ($appCount == 1) { + $appName = $stemSection; + } else { + $appName = $appName . " - " . $stemSection; + } + } + } + $appCount += 1; + } + //changed the way email list are displayed to actually show lists email address. + if ($appName == 'sympa - internet2' || $appName == 'sympa - incommon') { + if ($appName == 'sympa - internet2') { + $appName = $tempGroup['WGName'] . '@lists.' . 'internet2.edu'; + } else { + $appName = $tempGroup['WGName'] . '@lists.' . 'incommon.org'; + } + + } + $tempGroup['WGApp'] = $appName; + if ($method == 'member') { + // Screening out groups owned since this page show memberships not ownership! +// if ($tempGroup['WGRole'] !== 'admin' && $tempGroup['WGRole'] !== 'owner' && !$mainGroup ) { +// $workingGroups[] = $tempGroup; +// } + if(!$mainGroup) { + $workingGroups[] = $tempGroup; + unset($groups[$arrayIndex]); + } + } else { + $workingGroups[] = $tempGroup; + unset($groups[$arrayIndex]); + } + } + } + $arrayIndex += 1; + } + $finalWorkingGroups = array(); + + foreach ($workingGroups as $workingGroup) { + //Need to set first group in final Working Group array + if (count($finalWorkingGroups) == 0) { + $finalWorkingGroups[] = array( + 'WGName' => $workingGroup['WGName'], + 'WGShowName' => $workingGroup['WGShowName'], + 'Groups' => array($workingGroup) + ); + } else { + $foundMatch = false; + foreach ($finalWorkingGroups as &$finalWorkingGroup) { + if ($finalWorkingGroup['WGName'] == $workingGroup['WGName']) { + $finalWorkingGroup['WGShowName'] = $workingGroup['WGShowName']; + $finalWorkingGroup['Groups'][] = $workingGroup; + $foundMatch = true; + } + } + if (!$foundMatch) { + $finalWorkingGroups[] = array( + 'WGName' => $workingGroup['WGName'], + 'WGShowName' => $workingGroup['WGShowName'], + 'Groups' => array($workingGroup) + ); + } + } + } + + $friendlyGroups = $this->getFriendlyName(array_values($groups)); - $resultSet = $this->$method($conditions); + //Now need to add the groups back together for one set + foreach ($friendlyGroups as $friendlyGroup) { + $finalWorkingGroups[] = $friendlyGroup; + } - return $this->paginateRecords($resultSet, $conditions); + return $finalWorkingGroups; } + /** - * Parses returned records to display only requested records for pagination. + * Determine if result set contains friendly name, if so add as a new attribute in result set + * NOTE: Used by Ad-Hoc groups only, since WG are listed under a Grouping * - * @param array $recSet All Grouper Records associated to User's request - * @param array $conditions Listing of conditions for display of records with pagination - * @return array Records requested by user with pagination support + * @param array $groups Current result set of Groups + * @return array Same result set with added param for friendly name * */ - private function paginateRecords(array $recSet, array $conditions) { - - //set for pagination record count - $this->totalRecords = count($recSet); + private function getFriendlyName(array $groups) + { - //Return all, if requested - if (strtolower($conditions['limit']) == 'all') { - return $recSet; - } - - //Now slice recordset to return correct set of records. - $page = $conditions['page'] - 1; - $limit = $conditions['limit']; - if ($page == 0) { - $start = 0; - } else { - $start = $page * $limit; + //According to Jira ICPCO-200, will only display the last section of the stem in UI and then hover over for rest. + foreach ($groups as &$group) { + $friendlySections = explode(':', $group['displayName']); + $group['friendlyName'] = end($friendlySections); } - $end = $start + $limit; - return array_slice($recSet, $start, $end); + return $groups; } - /** - * Needed method to support Cake's pagination in the views - * - * @param null $conditions Listing of conditions for display of records with pagination - * @param int $recursive - * @param array $extra - * @return int Total number of Grouper Records returned by Grouper API call - * - */ - public function paginateCount($conditions = null, $recursive = 0, - $extra = array()) { - return $this->totalRecords; - } } diff --git a/PlantUML/1.puml b/PlantUML/1.puml new file mode 100644 index 0000000..86c136e --- /dev/null +++ b/PlantUML/1.puml @@ -0,0 +1,75 @@ +# Data from API call to get all Groups belong to +@startjson +#highlight "WsGetGroupsLiteResult" / "wsGroups" +#highlight "WsGetGroupsLiteResult" / "wsGroups" / "0" / "name" +#highlight "WsGetGroupsLiteResult" / "wsGroups" / "0" / "displayName" +#highlight "WsGetGroupsLiteResult" / "wsGroups" / "0" / "description" +#highlight "WsGetGroupsLiteResult" / "wsGroups" / "1" / "name" +#highlight "WsGetGroupsLiteResult" / "wsGroups" / "1" / "displayName" +#highlight "WsGetGroupsLiteResult" / "wsGroups" / "1" / "description" +#highlight "WsGetGroupsLiteResult" / "wsGroups" / "2" / "name" +#highlight "WsGetGroupsLiteResult" / "wsGroups" / "2" / "displayName" +#highlight "WsGetGroupsLiteResult" / "wsGroups" / "2" / "description" +#highlight "WsGetGroupsLiteResult" / "wsGroups" / "3" / "name" +#highlight "WsGetGroupsLiteResult" / "wsGroups" / "3" / "displayName" +#highlight "WsGetGroupsLiteResult" / "wsGroups" / "3" / "description" +{ + "WsGetGroupsLiteResult": { + "resultMetadata": { + "success": "T", + "resultCode": "SUCCESS" + }, + "wsSubject": { + "...": "..." + }, + "responseMetadata": { + "...": "..." + }, + "wsGroups": [ + { + "extension": "CO_members_active", + "displayName": "app:comanage-provision:CO_members_active", + "description": "Internet2 Collaborations Active Members", + "uuid": "f5dae468b9d9429993992781712c2f83", + "enabled": "T", + "displayExtension": "CO_members_active", + "name": "app:comanage-provision:CO_members_active", + "typeOfGroup": "group", + "idIndex": "11104" + }, + { + "extension": "grouperUiUserData", + "displayName": "etc:grouperUi:grouperUiUserData", + "description": "Internal group for grouper which has ...", + "uuid": "2748e23e51174145a4dc4d9e115c59da", + "enabled": "T", + "displayExtension": "grouperUiUserData", + "name": "etc:grouperUi:grouperUiUserData", + "typeOfGroup": "group", + "idIndex": "10015" + }, + { + "extension": "AdministeredByUniconFolks", + "displayName": "sandbox:UniconTest:Administered By Unicon Folks", + "description": "Description goes here", + "uuid": "0b26aa411a99405b9440be3d0b18dafa", + "enabled": "T", + "displayExtension": "Administered By Unicon Folks", + "name": "sandbox:UniconTest:AdministeredByUniconFolks", + "typeOfGroup": "group", + "idIndex": "18451" + }, + { + "extension": "MembersFromUniconAZ", + "displayName": "sandbox:UniconTest:Members At Unicon in AZ", + "uuid": "7a9f3b9837024a56b12a2a6259d520e0", + "enabled": "T", + "displayExtension": "Members At Unicon in AZ", + "name": "sandbox:UniconTest:MembersFromUniconAZ", + "typeOfGroup": "group", + "idIndex": "18447" + } + ] + } +} +@endjson \ No newline at end of file diff --git a/PlantUML/2.puml b/PlantUML/2.puml new file mode 100644 index 0000000..2cfc608 --- /dev/null +++ b/PlantUML/2.puml @@ -0,0 +1,25 @@ +@startuml +skinparam titleBorderRoundCorner 15 +skinparam titleBorderThickness 2 +skinparam titleBorderColor red +skinparam titleBackgroundColor Aqua-CadetBlue + +title Mapping of //My Membership// page to API results + +map "** API Grouper (with example data) => My Membership Page **" as CC { + extension = CO_members_active => NA + displayName = app:comanage-provision:CO_members_active => Name + description = Internet2 Collaborations Active Members => Description + uuid = f5dae468b9d9429993992781712c2f83 => NA + enabled = T => NA + displayExtension = CO_members_active => NA + name = app:comanage-provision:CO_members_active => Name + typeOfGroup = group => NA + idIndex = 11104 => NA +} + +note right of CC + Name Fields are used to determine Friendly Name +end note + +@enduml \ No newline at end of file diff --git a/PlantUML/3.puml b/PlantUML/3.puml new file mode 100644 index 0000000..16c8503 --- /dev/null +++ b/PlantUML/3.puml @@ -0,0 +1,46 @@ +@startuml +skinparam titleBorderRoundCorner 15 +skinparam titleBorderThickness 2 +skinparam titleBorderColor red +skinparam titleBackgroundColor Aqua-CadetBlue + +title Current Friendly name process for\nNon Working Groups + +start +:GET "Groups Member Of"; +:Group 1 +{ + "extension": "UniconMemberGroup", + "displayName": "sandbox:UniconTest:Unicon Members Group", + "uuid": "35c1ae4d9529492aac8cb2acb970279b", + "enabled": "T", + "displayExtension": "Unicon Members Group", + "name": "sandbox:UniconTest:UniconMemberGroup", + "typeOfGroup": "group", + "idIndex": "18446" +}; +partition "Compare Displayname and Name Params for Group 1" { +:Data: +**displayName** = "sandbox:UniconTest:Unicon Members Group" +**name** = "sandbox:UniconTest:UniconMemberGroup"; +:**Logic: Compare each Stem section**; + +if (Does "sandbox" == "sandbox"?) then (yes) + if (Does "UniconTest" == "UniconTest"?) then (yes) + if (Does "Unicon Members Group" == "UniconMemberGroup"?) then (yes) + :**FriendlyName** = "sandbox:UniconTest:UniconMemberGroup" + (No variance between **name** and **displayName**); + else (no) + :**FriendlyName** = "Unicon Members Group"; + endif + else (no) + #red:**FriendlyName** = "Unicon Members Group"; + note right: Not possible + endif +else (no) + #red:**FriendlyName** = "UniconTest:Unicon Members Group"; + note: Not possible +endif +stop +} +@enduml \ No newline at end of file diff --git a/PlantUML/4.puml b/PlantUML/4.puml new file mode 100644 index 0000000..37a5912 --- /dev/null +++ b/PlantUML/4.puml @@ -0,0 +1,64 @@ +# Current Friendly name process for Working Groups that is broken +@startuml +skinparam titleBorderRoundCorner 15 +skinparam titleBorderThickness 2 +skinparam titleBorderColor red +skinparam titleBackgroundColor Aqua-CadetBlue + +title Current Friendly name process for\na Working Groups that is broken + +start +:GET "Groups Member Of"; +fork +:Group 1 +{ + "extension": "admins", + "displayName": "app:confluence:Axel Working Group:admins", + "description": "Admins of confluence space for working group. Axel's working group for testing", + "uuid": "35eaa34fd2d443e5a66a0a355505f69e", + "enabled": "T", + "displayExtension": "admins", + "name": "app:confluence:AxelWorkingGroup:admins", + "typeOfGroup": "group", + "idIndex": "19010" +}; +fork again +:Group 2 +{ + "extension": "users", + "displayName": "app:confluence:Axel Working Group:users", + "description": "Users of confluence space for working group. Axel's working group for testing", + "uuid": "163dc892fa8e484a9262e6e9fa619791", + "enabled": "T", + "displayExtension": "users", + "name": "app:confluence:AxelWorkingGroup:users", + "typeOfGroup": "group", + "idIndex": "19011" +}; +end fork +partition "Compare Displayname and Name Params from Group 2" { +:Group 2 Data: +**displayName** = "app:confluence:Axel Working Group:users" +**name** = "app:confluence:AxelWorkingGroup:users"; +:**Logic: Compare each Stem section**; + +if (Does "app" == "app"?) then (yes) + if (Does "confluence" == "confluence"?) then (yes) + if (Does "Axel Working Group" == "AxelWorkingGroup"?) then (yes) + :**FriendlyName** = "app:confluence:AxelWorkingGroup:users" + (No variance between **name** and **displayName**); + else (no) + #red:**FriendlyName** = "Axel Working Group"; + note right: Missing **:users** + endif + else (no) + #red:**FriendlyName** = "Axel Working Group:users"; + note right: Not possible + endif +else (no) + #red:**FriendlyName** = "confluence:Axel Working Group:users"; + note: Not possible +endif +stop +} +@enduml \ No newline at end of file diff --git a/PlantUML/5.puml b/PlantUML/5.puml new file mode 100644 index 0000000..3856ac4 --- /dev/null +++ b/PlantUML/5.puml @@ -0,0 +1,121 @@ +# Removed "enabled"", "typeOfGroup", ""uuid"" and "idIndex" from returned records for brevity +@startjson +{ +"wsGroups": [ + { + "extension": "CO_members_active", + "displayName": "app:comanage-provision:CO_members_active", + "description": "Internet2 Collaborations Active Members", + "displayExtension": "CO_members_active", + "name": "app:comanage-provision:CO_members_active", + "typeOfGroup": "group", + "idIndex": "11104" + }, + { + "extension": "admins", + "displayName": "app:confluence:Axel Working Group:admins", + "description": "Admins of confluence space for working group. Axel's working group for testing", + "displayExtension": "admins", + "name": "app:confluence:AxelWorkingGroup:admins" + }, + { + "extension": "users", + "displayName": "app:confluence:Axel Working Group:users", + "description": "Users of confluence space for working group. Axel's working group for testing", + "displayExtension": "users", + "name": "app:confluence:AxelWorkingGroup:users" + }, + { + "extension": "admins", + "displayName": "app:confluence:NewWorkingGroupTest1155:admins", + "description": "Admins of confluence space for working group. NewWorkingGroupTest1155", + "displayExtension": "admins", + "name": "app:confluence:NewWorkingGroupTest1155:admins" + }, + { + "extension": "admins", + "displayName": "app:jira:AxelWorkingGroup:admins", + "description": "Users of jira project for working group. Axel's working group for testing", + "displayExtension": "admins", + "name": "app:jira:AxelWorkingGroup:admins" + }, + { + "extension": "users", + "displayName": "app:jira:AxelWorkingGroup:users", + "description": "Subscribers list receives working group emails. Axel's working group for testing", + "displayExtension": "users", + "name": "app:jira:AxelWorkingGroup:users" + }, + { + "extension": "admins", + "displayName": "app:jira:NewWorkingGroupTest1155:admins", + "description": "Users of jira project for working group. NewWorkingGroupTest1155", + "displayExtension": "admins", + "name": "app:jira:NewWorkingGroupTest1155:admins" + }, + { + "extension": "owners", + "displayName": "app:sympa:internet2:AxelWorkingGroup:owners", + "description": "Owners list manages the email list for the working group. Axel's working group for testing", + "displayExtension": "owners", + "name": "app:sympa:internet2:AxelWorkingGroup:owners" + }, + { + "extension": "subscribers", + "displayName": "app:sympa:internet2:AxelWorkingGroup:subscribers", + "description": "Subscribers list receives working group emails. Axel's working group for testing", + "displayExtension": "subscribers", + "name": "app:sympa:internet2:AxelWorkingGroup:subscribers" + }, + { + "extension": "owners", + "displayName": "app:sympa:internet2:NewWorkingGroupTest1155:owners", + "description": "Owners list manages the email list for the working group. NewWorkingGroupTest1155", + "displayExtension": "owners", + "name": "app:sympa:internet2:NewWorkingGroupTest1155:owners" + }, + { + "extension": "grouperUiUserData", + "displayName": "etc:grouperUi:grouperUiUserData", + "description": "Internal group for grouper which has user data stored ...", + "displayExtension": "grouperUiUserData", + "name": "etc:grouperUi:grouperUiUserData" + }, + { + "extension": "sysadmingroup", + "displayName": "etc:sysadmingroup", + "description": "system administrators with all privileges", + "displayExtension": "sysadmingroup", + "name": "etc:sysadmingroup" + }, + { + "extension": "admins", + "displayName": "ref:InCommon-collab:AxelWorkingGroup:AxelWorkingGroup admins", + "description": "Admins role means can manage / attest the working group. Axel's working group for testing", + "displayExtension": "AxelWorkingGroup admins", + "name": "ref:incommon-collab:AxelWorkingGroup:admins" + }, + { + "extension": "users", + "displayName": "ref:InCommon-collab:AxelWorkingGroup:AxelWorkingGroup users", + "description": "Users role means members of the working group with access to collaboration tools. Axel's working group for testing", + "displayExtension": "AxelWorkingGroup users", + "name": "ref:incommon-collab:AxelWorkingGroup:users" + }, + { + "extension": "admins", + "displayName": "ref:InCommon-collab:NewWorkingGroupTest1155:NewWorkingGroupTest1155 admins", + "description": "Admins role means can manage / attest the working group. NewWorkingGroupTest1155", + "displayExtension": "NewWorkingGroupTest1155 admins", + "name": "ref:incommon-collab:NewWorkingGroupTest1155:admins" + }, + { + "extension": "workinggroupadmins", + "displayName": "ref:workinggroupadmins", + "description": "Being a member of this group enables you to create collaboration groups under ...", + "displayExtension": "workinggroupadmins", + "name": "ref:workinggroupadmins" + } + ] +} +@endjson \ No newline at end of file diff --git a/PlantUML/6.puml b/PlantUML/6.puml new file mode 100644 index 0000000..0999820 --- /dev/null +++ b/PlantUML/6.puml @@ -0,0 +1,27 @@ +# Info taken from https://spaces.at.internet2.edu/display/Grouper/Grouper+custom+template+via+GSH+Internet2+example +@startuml +skinparam titleBorderRoundCorner 15 +skinparam titleBorderThickness 2 +skinparam titleBorderColor red +skinparam titleBackgroundColor Aqua-CadetBlue + +title Process groups to verify if part of\na Working Group + +start +:Working Group Stems: +---- +* "app:jira" +* "app:confluence" +* "app:sympa:internet2" +* "app:sympa:incommon" +* "ref:InCommon-collab"; + +partition "For Each Group" { +if (Does "app:jira:AxelWorkingGroup:users" start with any **Working Group Stem**?) then (yes) + :Hold as part of a Working Group; +else (no) + :Next record"; +endif +} +stop +@enduml \ No newline at end of file diff --git a/PlantUML/7.puml b/PlantUML/7.puml new file mode 100644 index 0000000..24616f8 --- /dev/null +++ b/PlantUML/7.puml @@ -0,0 +1,63 @@ +@startuml +skinparam titleBorderRoundCorner 15 +skinparam titleBorderThickness 2 +skinparam titleBorderColor red +skinparam titleBackgroundColor Aqua-CadetBlue + +title With records held as part of\na Working Group + +start +:GET "Groups Member Of"; +fork +:Group 1 +{ + "extension": "admins", + "displayName": "app:confluence:Bill Working Group:admins", + "description": "Admins of confluence space for working group." + "displayExtension": "admins", + "name": "app:confluence:BillWorkingGroup:admins" +}; +fork again +:Group 2 +{ + "extension": "users", + "displayName": "app:confluence:Axel Working Group:users", + "description": "Users of confluence space for working group." + "displayExtension": "users", + "name": "app:confluence:AxelWorkingGroup:users" +}; +fork again +:Group 3 +{ + "extension": "admins", + "displayName": "app:jira:AxelWorkingGroup:admins", + "description": "Users of jira project for working group." + "displayExtension": "admins", + "name": "app:jira:AxelWorkingGroup:admins" +}; +end fork +partition "Compare Each Group to see if same Working Group" { +:Compare Group 1 and Group 2 +**Group 1 name** = "app:confluence:BillWorkingGroup:admins" +**Group 2 name** = "app:confluence:AxelWorkingGroup:users"; +:Assumption = Last section of name value is **ALWAYS** Members (users, admins) , therefore second to last section is Working Group Name; +if (Does "BillWorkingGroup" == "AxelWorkingGroup"?) then (yes) + :Save to BillWorkingGroup; +endif +:Compare Group 2 and Group 3; +if (Does "AxelWorkingGroup" == "AxelWorkingGroup"?) then (yes) + :Save to AxelWorkingGroup; +endif +} +stop + +:=//My Membership:// +---- +**BillWorkingGroup:** +* Confluence Admins +---- +**Axel Working Group:** +* Confluence Users +* Jira Admins; + +@enduml \ No newline at end of file diff --git a/PlantUML/GroupsBelongTo.puml b/PlantUML/GroupsBelongTo.puml new file mode 100644 index 0000000..b8c4fc9 --- /dev/null +++ b/PlantUML/GroupsBelongTo.puml @@ -0,0 +1,219 @@ + +# Removed "enabled"", "typeOfGroup", ""uuid"" and "idIndex" from returned records for brevity +@startjson +{ +"wsGroups": [ + { + "extension": "CO_members_active", + "displayName": "app:comanage-provision:CO_members_active", + "description": "Internet2 Collaborations Active Members", + "displayExtension": "CO_members_active", + "name": "app:comanage-provision:CO_members_active", + "typeOfGroup": "group", + "idIndex": "11104" + }, + { + "extension": "admins", + "displayName": "app:confluence:Axel Working Group:admins", + "description": "Admins of confluence space for working group. Axel's working group for testing", + "displayExtension": "admins", + "name": "app:confluence:ExampleWGName:admins" + }, + { + "extension": "users", + "displayName": "app:confluence:Axel Working Group:users", + "description": "Users of confluence space for working group. Axel's working group for testing", + "displayExtension": "users", + "name": "app:confluence:ExampleWGName:users" + }, + { + "extension": "admins", + "displayName": "app:confluence:NewWorkingGroupTest1155:admins", + "description": "Admins of confluence space for working group. NewWorkingGroupTest1155", + "displayExtension": "admins", + "name": "app:confluence:NewWorkingGroupTest1155:admins" + }, + { + "extension": "admins", + "displayName": "app:jira:ExampleWGName:admins", + "description": "Users of jira project for working group. Axel's working group for testing", + "displayExtension": "admins", + "name": "app:jira:ExampleWGName:admins" + }, + { + "extension": "users", + "displayName": "app:jira:ExampleWGName:users", + "description": "Subscribers list receives working group emails. Axel's working group for testing", + "displayExtension": "users", + "name": "app:jira:ExampleWGName:users" + }, + { + "extension": "admins", + "displayName": "app:jira:NewWorkingGroupTest1155:admins", + "description": "Users of jira project for working group. NewWorkingGroupTest1155", + "displayExtension": "admins", + "name": "app:jira:NewWorkingGroupTest1155:admins" + }, + { + "extension": "owners", + "displayName": "app:sympa:internet2:ExampleWGName:owners", + "description": "Owners list manages the email list for the working group. Axel's working group for testing", + "displayExtension": "owners", + "name": "app:sympa:internet2:ExampleWGName:owners" + }, + { + "extension": "subscribers", + "displayName": "app:sympa:internet2:ExampleWGName:subscribers", + "description": "Subscribers list receives working group emails. Axel's working group for testing", + "displayExtension": "subscribers", + "name": "app:sympa:internet2:ExampleWGName:subscribers" + }, + { + "extension": "owners", + "displayName": "app:sympa:internet2:NewWorkingGroupTest1155:owners", + "description": "Owners list manages the email list for the working group. NewWorkingGroupTest1155", + "displayExtension": "owners", + "name": "app:sympa:internet2:NewWorkingGroupTest1155:owners" + }, + { + "extension": "grouperUiUserData", + "displayName": "etc:grouperUi:grouperUiUserData", + "description": "Internal group for grouper which has user data stored ...", + "displayExtension": "grouperUiUserData", + "name": "etc:grouperUi:grouperUiUserData" + }, + { + "extension": "sysadmingroup", + "displayName": "etc:sysadmingroup", + "description": "system administrators with all privileges", + "displayExtension": "sysadmingroup", + "name": "etc:sysadmingroup" + }, + { + "extension": "admins", + "displayName": "ref:InCommon-collab:ExampleWGName:ExampleWGName admins", + "description": "Admins role means can manage / attest the working group. Axel's working group for testing", + "displayExtension": "ExampleWGName admins", + "name": "ref:incommon-collab:ExampleWGName:admins" + }, + { + "extension": "users", + "displayName": "ref:InCommon-collab:ExampleWGName:ExampleWGName users", + "description": "Users role means members of the working group with access to collaboration tools. Axel's working group for testing", + "displayExtension": "ExampleWGName users", + "name": "ref:incommon-collab:ExampleWGName:users" + }, + { + "extension": "admins", + "displayName": "ref:InCommon-collab:NewWorkingGroupTest1155:NewWorkingGroupTest1155 admins", + "description": "Admins role means can manage / attest the working group. NewWorkingGroupTest1155", + "displayExtension": "NewWorkingGroupTest1155 admins", + "name": "ref:incommon-collab:NewWorkingGroupTest1155:admins" + }, + { + "extension": "workinggroupadmins", + "displayName": "ref:workinggroupadmins", + "description": "Being a member of this group enables you to create collaboration groups under ...", + "displayExtension": "workinggroupadmins", + "name": "ref:workinggroupadmins" + } + ] +} +@endjson + + +============================================================= + +# Info taken from https://spaces.at.internet2.edu/display/Grouper/Grouper+custom+template+via+GSH+Internet2+example +@startuml +skinparam titleBorderRoundCorner 15 +skinparam titleBorderThickness 2 +skinparam titleBorderColor red +skinparam titleBackgroundColor Aqua-CadetBlue + +title Process groups to verify if part of\na Working Group + +start +:Working Group Stems: +---- +* "app:jira" +* "app:confluence" +* "app:sympa:internet2" +* "app:sympa:incommon" +* "ref:InCommon-collab"; + +partition "For Each Group" { +if (Does "app:jira:ExampleWGName:users" start with any **Working Group Stem**?) then (yes) + :Hold as part of a Working Group; +else (no) + :Next record"; +endif +} +stop +@enduml + +============================================================= + +@startuml +skinparam titleBorderRoundCorner 15 +skinparam titleBorderThickness 2 +skinparam titleBorderColor red +skinparam titleBackgroundColor Aqua-CadetBlue + +title With records held as part of\na Working Group + +start +:GET "Groups Member Of"; +fork +:Group 1 +{ + "extension": "admins", + "displayName": "app:confluence:Bill Working Group:admins", + "description": "Admins of confluence space for working group." + "displayExtension": "admins", + "name": "app:confluence:BillWorkingGroup:admins" +}; +fork again +:Group 2 +{ + "extension": "users", + "displayName": "app:confluence:Axel Working Group:users", + "description": "Users of confluence space for working group." + "displayExtension": "users", + "name": "app:confluence:ExampleWGName:users" +}; +fork again +:Group 3 +{ + "extension": "admins", + "displayName": "app:jira:ExampleWGName:admins", + "description": "Users of jira project for working group." + "displayExtension": "admins", + "name": "app:jira:ExampleWGName:admins" +}; +end fork +partition "Compare Each Group to see if same Working Group" { +:Compare Group 1 and Group 2 +**Group 1 name** = "app:confluence:BillWorkingGroup:admins" +**Group 2 name** = "app:confluence:ExampleWGName:users"; +:Assumption = Last section of name value is **ALWAYS** Members (users, admins) , therefore second to last section is Working Group Name; +if (Does "BillWorkingGroup" == "ExampleWGName"?) then (yes) + :Save to BillWorkingGroup; +endif +:Compare Group 2 and Group 3; +if (Does "ExampleWGName" == "ExampleWGName"?) then (yes) + :Save to ExampleWGName; +endif +} +stop + +:=//My Membership:// +---- +**BillWorkingGroup:** +* Confluence Admins +---- +**Axel Working Group:** +* Confluence Users +* Jira Admins; + +@enduml \ No newline at end of file diff --git a/PlantUML/MyMemberProcess.puml b/PlantUML/MyMemberProcess.puml new file mode 100644 index 0000000..211ce24 --- /dev/null +++ b/PlantUML/MyMemberProcess.puml @@ -0,0 +1,219 @@ +# Data from API call to get all Groups belong to +@startjson +#highlight "WsGetGroupsLiteResult" / "wsGroups" +#highlight "WsGetGroupsLiteResult" / "wsGroups" / "0" / "name" +#highlight "WsGetGroupsLiteResult" / "wsGroups" / "0" / "displayName" +#highlight "WsGetGroupsLiteResult" / "wsGroups" / "0" / "description" +#highlight "WsGetGroupsLiteResult" / "wsGroups" / "1" / "name" +#highlight "WsGetGroupsLiteResult" / "wsGroups" / "1" / "displayName" +#highlight "WsGetGroupsLiteResult" / "wsGroups" / "1" / "description" +#highlight "WsGetGroupsLiteResult" / "wsGroups" / "2" / "name" +#highlight "WsGetGroupsLiteResult" / "wsGroups" / "2" / "displayName" +#highlight "WsGetGroupsLiteResult" / "wsGroups" / "2" / "description" +#highlight "WsGetGroupsLiteResult" / "wsGroups" / "3" / "name" +#highlight "WsGetGroupsLiteResult" / "wsGroups" / "3" / "displayName" +#highlight "WsGetGroupsLiteResult" / "wsGroups" / "3" / "description" +{ + "WsGetGroupsLiteResult": { + "resultMetadata": { + "success": "T", + "resultCode": "SUCCESS" + }, + "wsSubject": { + "...": "..." + }, + "responseMetadata": { + "...": "..." + }, + "wsGroups": [ + { + "extension": "CO_members_active", + "displayName": "app:comanage-provision:CO_members_active", + "description": "Internet2 Collaborations Active Members", + "uuid": "f5dae468b9d9429993992781712c2f83", + "enabled": "T", + "displayExtension": "CO_members_active", + "name": "app:comanage-provision:CO_members_active", + "typeOfGroup": "group", + "idIndex": "11104" + }, + { + "extension": "grouperUiUserData", + "displayName": "etc:grouperUi:grouperUiUserData", + "description": "Internal group for grouper which has ...", + "uuid": "2748e23e51174145a4dc4d9e115c59da", + "enabled": "T", + "displayExtension": "grouperUiUserData", + "name": "etc:grouperUi:grouperUiUserData", + "typeOfGroup": "group", + "idIndex": "10015" + }, + { + "extension": "AdministeredByUniconFolks", + "displayName": "sandbox:UniconTest:Administered By Unicon Folks", + "description": "Description goes here", + "uuid": "0b26aa411a99405b9440be3d0b18dafa", + "enabled": "T", + "displayExtension": "Administered By Unicon Folks", + "name": "sandbox:UniconTest:AdministeredByUniconFolks", + "typeOfGroup": "group", + "idIndex": "18451" + }, + { + "extension": "MembersFromUniconAZ", + "displayName": "sandbox:UniconTest:Members At Unicon in AZ", + "uuid": "7a9f3b9837024a56b12a2a6259d520e0", + "enabled": "T", + "displayExtension": "Members At Unicon in AZ", + "name": "sandbox:UniconTest:MembersFromUniconAZ", + "typeOfGroup": "group", + "idIndex": "18447" + } + ] + } +} +@endjson + +============================================================= + +@startuml +skinparam titleBorderRoundCorner 15 +skinparam titleBorderThickness 2 +skinparam titleBorderColor red +skinparam titleBackgroundColor Aqua-CadetBlue + +title Mapping of //My Membership// page to API results + +map "** API Grouper (with example data) => My Membership Page **" as CC { + extension = CO_members_active => NA + displayName = app:comanage-provision:CO_members_active => Name + description = Internet2 Collaborations Active Members => Description + uuid = f5dae468b9d9429993992781712c2f83 => NA + enabled = T => NA + displayExtension = CO_members_active => NA + name = app:comanage-provision:CO_members_active => Name + typeOfGroup = group => NA + idIndex = 11104 => NA +} + +note right of CC + Name Fields are used to determine Friendly Name +end note + +@enduml + +============================================================= + +@startuml +skinparam titleBorderRoundCorner 15 +skinparam titleBorderThickness 2 +skinparam titleBorderColor red +skinparam titleBackgroundColor Aqua-CadetBlue + +title Current Friendly name process for\nNon Working Groups + +start +:GET "Groups Member Of"; +:Group 1 +{ + "extension": "UniconMemberGroup", + "displayName": "sandbox:UniconTest:Unicon Members Group", + "uuid": "35c1ae4d9529492aac8cb2acb970279b", + "enabled": "T", + "displayExtension": "Unicon Members Group", + "name": "sandbox:UniconTest:UniconMemberGroup", + "typeOfGroup": "group", + "idIndex": "18446" +}; +partition "Compare Displayname and Name Params for Group 1" { +:Data: +**displayName** = "sandbox:UniconTest:Unicon Members Group" +**name** = "sandbox:UniconTest:UniconMemberGroup"; +:**Logic: Compare each Stem section**; + +if (Does "sandbox" == "sandbox"?) then (yes) + if (Does "UniconTest" == "UniconTest"?) then (yes) + if (Does "Unicon Members Group" == "UniconMemberGroup"?) then (yes) + :**FriendlyName** = "sandbox:UniconTest:UniconMemberGroup" + (No variance between **name** and **displayName**); + else (no) + :**FriendlyName** = "Unicon Members Group"; + endif + else (no) + #red:**FriendlyName** = "Unicon Members Group"; + note right: Not possible + endif +else (no) + #red:**FriendlyName** = "UniconTest:Unicon Members Group"; + note: Not possible +endif +stop +} +@enduml + +============================================================= + +# Current Friendly name process for Working Groups that is broken +@startuml +skinparam titleBorderRoundCorner 15 +skinparam titleBorderThickness 2 +skinparam titleBorderColor red +skinparam titleBackgroundColor Aqua-CadetBlue + +title Current Friendly name process for\na Working Groups that is broken + +start +:GET "Groups Member Of"; +fork +:Group 1 +{ + "extension": "admins", + "displayName": "app:confluence:Axel Working Group:admins", + "description": "Admins of confluence space for working group. Axel's working group for testing", + "uuid": "35eaa34fd2d443e5a66a0a355505f69e", + "enabled": "T", + "displayExtension": "admins", + "name": "app:confluence:AxelWorkingGroup:admins", + "typeOfGroup": "group", + "idIndex": "19010" +}; +fork again +:Group 2 +{ + "extension": "users", + "displayName": "app:confluence:Axel Working Group:users", + "description": "Users of confluence space for working group. Axel's working group for testing", + "uuid": "163dc892fa8e484a9262e6e9fa619791", + "enabled": "T", + "displayExtension": "users", + "name": "app:confluence:AxelWorkingGroup:users", + "typeOfGroup": "group", + "idIndex": "19011" +}; +end fork +partition "Compare Displayname and Name Params from Group 2" { +:Group 2 Data: +**displayName** = "app:confluence:Axel Working Group:users" +**name** = "app:confluence:AxelWorkingGroup:users"; +:**Logic: Compare each Stem section**; + +if (Does "app" == "app"?) then (yes) + if (Does "confluence" == "confluence"?) then (yes) + if (Does "Axel Working Group" == "AxelWorkingGroup"?) then (yes) + :**FriendlyName** = "app:confluence:AxelWorkingGroup:users" + (No variance between **name** and **displayName**); + else (no) + #red:**FriendlyName** = "Axel Working Group"; + note right: Missing **:users** + endif + else (no) + #red:**FriendlyName** = "Axel Working Group:users"; + note right: Not possible + endif +else (no) + #red:**FriendlyName** = "confluence:Axel Working Group:users"; + note: Not possible +endif +stop +} +@enduml diff --git a/View/CoGrouperLites/display.ctp b/View/CoGrouperLites/display.ctp index 6d1e03e..480ee25 100644 --- a/View/CoGrouperLites/display.ctp +++ b/View/CoGrouperLites/display.ctp @@ -25,7 +25,7 @@ * * @link http://www.internet2.edu/comanage COmanage Project * @package registry - * @since COmanage Registry v3.2.0 + * @since COmanage Registry v4.1.1 * @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) */ @@ -33,98 +33,91 @@ $divid = $vv_config['CoGrouperLite']['co_dashboard_widget_id']; +// $coid = $config['co']; +// $glid = $config['glid']; + +$this->extend('/GrouperGroups/base'); echo $this->element('GrouperLite.base-styles'); +$idsuffix = rand(); + ?> + -
+
-
-
+
+
-
+
Html->image('GrouperLite.grouper-logo.png', array('class' => 'logo-fluid')); ?>
- -
-
  Email Lists
- diff --git a/View/CoGrouperLites/fields.inc b/View/CoGrouperLites/fields.inc index 3811a43..4927142 100644 --- a/View/CoGrouperLites/fields.inc +++ b/View/CoGrouperLites/fields.inc @@ -72,28 +72,29 @@ print $this->Form->hidden('co_dashboard_widget_id', array('default' => $vv_dwid)
  • - Form->label('connUrl', _txt('pl.grouperlite.config.grouper-url')) : _txt('pl.grouperlite.config.grouper-url')); ?> + Form->label('grouper_url', _txt('pl.grouperlite.config.grouper-url')) : _txt('pl.grouperlite.config.grouper-url')); ?> * +
    + Form->label('grouper_url', _txt('pl.grouperlite.config.grouper-url-subscript')) : _txt('pl.grouperlite.config.grouper-url-subscript')); ?> +
    Form->input('connUrl', $attrs); + print $this->Form->input('grouper_url', $attrs); - if ($this->Form->isFieldError('connUrl')) { - print $this->Form->error('connUrl'); + if ($this->Form->isFieldError('grouper_url')) { + print $this->Form->error('grouper_url'); } } else { - if (!empty($co_grouper_lites[0]['CoGrouperLite']['connUrl'])) { - print filter_var($co_grouper_lites[0]['CoGrouperLite']['connUrl'], FILTER_SANITIZE_SPECIAL_CHARS); + if (!empty($co_grouper_lites[0]['CoGrouperLite']['grouper_url'])) { + print filter_var($co_grouper_lites[0]['CoGrouperLite']['grouper_url'], FILTER_SANITIZE_SPECIAL_CHARS); } } ?> @@ -102,28 +103,26 @@ print $this->Form->hidden('co_dashboard_widget_id', array('default' => $vv_dwid)
  • - Form->label('connVer', _txt('pl.grouperlite.config.grouper-version')) : _txt('pl.grouperlite.config.grouper-version')); ?> + Form->label('conn_url', _txt('pl.grouperlite.config.grouper-ws-url')) : _txt('pl.grouperlite.config.grouper-url')); ?> *
    Form->input('connVer', $attrs); + print $this->Form->input('conn_url', $attrs); - if ($this->Form->isFieldError('connVer')) { - print $this->Form->error('connVer'); + if ($this->Form->isFieldError('conn_url')) { + print $this->Form->error('conn_url'); } } else { - if (!empty($co_grouper_lites[0]['CoGrouperLite']['connVer'])) { - print filter_var($co_grouper_lites[0]['CoGrouperLite']['connVer'], FILTER_SANITIZE_SPECIAL_CHARS); + if (!empty($co_grouper_lites[0]['CoGrouperLite']['conn_url'])) { + print filter_var($co_grouper_lites[0]['CoGrouperLite']['conn_url'], FILTER_SANITIZE_SPECIAL_CHARS); } } ?> @@ -132,28 +131,26 @@ print $this->Form->hidden('co_dashboard_widget_id', array('default' => $vv_dwid)
  • - Form->label('connUser', _txt('pl.grouperlite.config.grouper-un')) : _txt('pl.grouperlite.config.grouper-un')); ?> + Form->label('conn_ver', _txt('pl.grouperlite.config.grouper-ws-version')) : _txt('pl.grouperlite.config.grouper-version')); ?> *
    Form->input('connUser', $attrs); + print $this->Form->input('conn_ver', $attrs); - if ($this->Form->isFieldError('connUser')) { - print $this->Form->error('connUser'); + if ($this->Form->isFieldError('conn_ver')) { + print $this->Form->error('conn_ver'); } } else { - if (!empty($co_grouper_lites[0]['CoGrouperLite']['connUser'])) { - print filter_var($co_grouper_lites[0]['CoGrouperLite']['connUser'], FILTER_SANITIZE_SPECIAL_CHARS); + if (!empty($co_grouper_lites[0]['CoGrouperLite']['conn_ver'])) { + print filter_var($co_grouper_lites[0]['CoGrouperLite']['conn_ver'], FILTER_SANITIZE_SPECIAL_CHARS); } } ?> @@ -162,28 +159,142 @@ print $this->Form->hidden('co_dashboard_widget_id', array('default' => $vv_dwid)
  • - Form->label('connPass', _txt('pl.grouperlite.config.grouper-pw')) : _txt('pl.grouperlite.config.grouper-pw')); ?> + Form->label('conn_user', _txt('pl.grouperlite.config.grouper-ws-un')) : _txt('pl.grouperlite.config.grouper-un')); ?> *
    Form->password('connPass', $attrs); + print $this->Form->input('conn_user', $attrs); - if ($this->Form->isFieldError('connPass')) { - print $this->Form->error('connPass'); + if ($this->Form->isFieldError('conn_user')) { + print $this->Form->error('conn_user'); } } else { - if (!empty($co_grouper_lites[0]['CoGrouperLite']['connPass'])) { - print filter_var($co_grouper_lites[0]['CoGrouperLite']['connPass'], FILTER_SANITIZE_SPECIAL_CHARS); + if (!empty($co_grouper_lites[0]['CoGrouperLite']['conn_user'])) { + print filter_var($co_grouper_lites[0]['CoGrouperLite']['conn_user'], FILTER_SANITIZE_SPECIAL_CHARS); + } + } + ?> +
    +
  • +
  • +
    +
    + Form->label('conn_pass', _txt('pl.grouperlite.config.grouper-ws-pw')) : _txt('pl.grouperlite.config.grouper-pw')); ?> + * +
    +
    +
    + Form->password('conn_pass', $attrs); + + if ($this->Form->isFieldError('conn_pass')) { + print $this->Form->error('conn_pass'); + } + } else { + if (!empty($co_grouper_lites[0]['CoGrouperLite']['conn_pass'])) { + print filter_var($co_grouper_lites[0]['CoGrouperLite']['conn_pass'], FILTER_SANITIZE_SPECIAL_CHARS); + } + } + ?> +
    +
  • + +
  • +
    +
    + Form->label('adhoc_heading', _txt('pl.grouperlite.config.ad-hoc-heading')) : _txt('pl.grouperlite.config.ad-hoc-heading')); ?> +
    +
    +
    + Form->input('adhoc_heading', $attrs); + + if ($this->Form->isFieldError('adhoc_heading')) { + print $this->Form->error('adhoc_heading'); + } + } else { + if (!empty($co_grouper_lites[0]['CoGrouperLite']['adhoc_heading'])) { + print filter_var($co_grouper_lites[0]['CoGrouperLite']['adhoc_heading'], FILTER_SANITIZE_SPECIAL_CHARS); + } + } + ?> +
    +
  • + +
  • +
    +
    + Form->label('wg_heading', _txt('pl.grouperlite.config.wg-heading')) : _txt('pl.grouperlite.config.wg-heading')); ?> +
    +
    +
    + Form->input('wg_heading', $attrs); + + if ($this->Form->isFieldError('wg_heading')) { + print $this->Form->error('wg_heading'); + } + } else { + if (!empty($co_grouper_lites[0]['CoGrouperLite']['wg_heading'])) { + print filter_var($co_grouper_lites[0]['CoGrouperLite']['wg_heading'], FILTER_SANITIZE_SPECIAL_CHARS); + } + } + ?> +
    +
  • + +
  • + +
    +
    + Form->label('default_collapse', _txt('pl.grouperlite.config.default-collapse')) : _txt('pl.grouperlite.config.default-collapse')); ?> +
    +
    +
    + Form->radio('default_collapse', array( + 'collapsed' => 'True', + 'expanded' => 'False' + ), array( + 'value' => $co_grouper_lites[0]['CoGrouperLite']['default_collapse'], + 'legend' => false, + 'separator' => '  ' + )); + + if ($this->Form->isFieldError('default_collapse')) { + print $this->Form->error('default_collapse'); + } + } else { + if (!empty($co_grouper_lites[0]['CoGrouperLite']['default_collapse'])) { + print filter_var($co_grouper_lites[0]['CoGrouperLite']['default_collapse'], FILTER_SANITIZE_SPECIAL_CHARS); } } ?> @@ -201,7 +312,6 @@ print $this->Form->hidden('co_dashboard_widget_id', array('default' => $vv_dwid)
  • -

    diff --git a/View/Elements/Components/groupattributes.ctp b/View/Elements/Components/groupattributes.ctp index e23f097..91f246f 100644 --- a/View/Elements/Components/groupattributes.ctp +++ b/View/Elements/Components/groupattributes.ctp @@ -1,26 +1,28 @@ - - - - - - - - - - + 0) : ?> +
    + - - - + + + - - -
    - - - -
    - + + + + + + + + + + + + + + + + +

    diff --git a/View/Elements/Components/navigation-emaillists.ctp b/View/Elements/Components/navigation-emaillists.ctp index 7597315..9a1b34a 100644 --- a/View/Elements/Components/navigation-emaillists.ctp +++ b/View/Elements/Components/navigation-emaillists.ctp @@ -3,24 +3,24 @@ @@ -29,7 +29,7 @@ print $this->Html->link( _txt('pl.grouperlite.nav.emaillists-manage'), array( - 'controller' => 'GrouperGroups', + 'controller' => 'grouper_groups', 'action' => 'emaillistsmanage' ), array('class' => array('nav-link', $active == 'emaillistsmanage' ? 'active' : '')) @@ -37,18 +37,18 @@ ?>
    - +
    Html->url( + $createTemplateGroupUrl = $this->Html->url( array( - 'controller' => 'groupergroups', - 'action' => 'groupcreatetemplateform' + 'controller' => 'grouper_groups', + 'action' => 'groupcreatetemplate' ) ); ?> - -   + +  
    @@ -56,8 +56,8 @@ Html->url( array( - 'controller' => 'groupergroups', - 'action' => 'groupoptin' + 'controller' => 'grouper_groups', + 'action' => 'groupmember' ) ); ?> diff --git a/View/Elements/Components/navigation-groups.ctp b/View/Elements/Components/navigation-groups.ctp index 636e6c4..f437f4c 100644 --- a/View/Elements/Components/navigation-groups.ctp +++ b/View/Elements/Components/navigation-groups.ctp @@ -3,24 +3,24 @@ @@ -29,7 +29,7 @@ print $this->Html->link( _txt('pl.grouperlite.nav.groups-presided'), array( - 'controller' => 'groupergroups', + 'controller' => 'grouper_groups', 'action' => 'groupowner' ), array('class' => array('nav-link', $active == 'groupowner' ? 'active' : '')) @@ -37,27 +37,44 @@ ?>
    - +
    Html->url( array( - 'controller' => 'groupergroups', - 'action' => 'groupcreateform' + 'controller' => 'grouper_groups', + 'action' => 'groupcreate' ) ); + $createGroupUrl = '#'; ?> - +  
    + + +
    + Html->url( + array( + 'controller' => 'grouper_groups', + 'action' => 'groupcreatetemplate' + ) + ); + ?> + +   + +
    + Html->url( array( - 'controller' => 'groupergroups', - 'action' => 'emaillistsoptin' + 'controller' => 'grouper_groups', + 'action' => 'emaillistsmember' ) ); ?> @@ -65,4 +82,5 @@  
    + */ ?> \ No newline at end of file diff --git a/View/Elements/Components/optAction.ctp b/View/Elements/Components/optAction.ctp index 2928dca..aeb0ea0 100644 --- a/View/Elements/Components/optAction.ctp +++ b/View/Elements/Components/optAction.ctp @@ -1,10 +1,12 @@ Form->create(false, array( - 'url' => array('controller' => 'groupergroups', 'action' => $action), - 'class' => 'd-flex justify-content-center' + 'url' => array('controller' => 'grouper_groups', 'action' => $action), + 'class' => 'd-flex justify-content-center', + 'id' => 'join-group.' . $idx )); ?> -Form->hidden('GroupName', array('default' => $group)); ?> - Form->end(); ?> \ No newline at end of file diff --git a/View/Elements/Components/search.ctp b/View/Elements/Components/search.ctp index ca42a44..ab982b6 100644 --- a/View/Elements/Components/search.ctp +++ b/View/Elements/Components/search.ctp @@ -1,40 +1,29 @@ - Form->create(false, array( - 'url' => array('controller' => 'groupergroups', 'action' => $active), + 'url' => array('controller' => 'grouper_groups', 'action' => $active), 'class' => 'search mb-4' )); ?>
    + +
    \ No newline at end of file diff --git a/View/Elements/Components/vue-table.ctp b/View/Elements/Components/vue-table.ctp new file mode 100644 index 0000000..af8e203 --- /dev/null +++ b/View/Elements/Components/vue-table.ctp @@ -0,0 +1,135 @@ + + + + +
    + element("pagination", array( + 'goto' => false, + 'limit' => false, + 'numbers' => false, + 'counter' => true, + 'class' => 'counter' + )); ?> +
    + + + +
    + + element("pagination", array( + 'goto' => false, + 'limit' => true, + 'numbers' => true, + 'counter' => false + )); ?> +
    diff --git a/View/Elements/base-styles.ctp b/View/Elements/base-styles.ctp index 00ead17..48ea5e2 100644 --- a/View/Elements/base-styles.ctp +++ b/View/Elements/base-styles.ctp @@ -1,8 +1,8 @@ \ No newline at end of file diff --git a/View/Elements/pagination.ctp b/View/Elements/pagination.ctp index 1f0d819..a182dbd 100644 --- a/View/Elements/pagination.ctp +++ b/View/Elements/pagination.ctp @@ -36,7 +36,7 @@ $includeGoto = isset($goto) ? $goto : true; ?> -
    +
    Html->meta( + array('http-equiv' => 'Cache-Control', 'content' => 'no-cache, no-store, must-revalidate'), + null, + array('inline' => false) +); +echo $this->Html->meta( + array('http-equiv' => 'Pragma', 'content' => 'no-cache'), + null, + array('inline' => false) +); +echo $this->Html->meta( + array('http-equiv' => 'Expires', 'content' => '0'), + null, + array('inline' => false) +); + print $this->element('GrouperLite.base-styles'); print $this->Html->css('GrouperLite.co-grouper-plugin') . "\n "; + + ?> - -Html->script('GrouperLite.typeahead.bundle.js') . "\n "; -$this->Html->addCrumb(_txt('pl.grouperlite.crumb.root'), array('controller' => 'groupergroups', 'action' => 'groupoptin'), array('prepend' => true)); +Html->addCrumb(_txt('pl.grouperlite.crumb.root'), array('controller' => 'grouper_groups', 'action' => 'groupmember'), array('prepend' => true)); ?> + +
    -
    - Html->image('GrouperLite.Grouper.jpg', array('class' => 'img-fluid mr-2', 'style' => 'height: 50px')); ?> -

    - -
    fetch('content'); ?>
    \ No newline at end of file diff --git a/View/GrouperGroups/emaillistinfo.ctp b/View/GrouperGroups/emaillistinfo.ctp deleted file mode 100644 index daa4379..0000000 --- a/View/GrouperGroups/emaillistinfo.ctp +++ /dev/null @@ -1,70 +0,0 @@ -extend('/GrouperGroups/base'); ?> - - -
    -
    -

    -
    - - - - -   - - -
    -
    -
    -
    -
    -

    - - - -
    - element('Components/groupproperties', array( - 'group' => $groupergroupsdetail - )); ?> -
    -
    -
    -

    - element('Components/groupattributes', array( - 'attributes' => $groupergroupsdetail['attributes'], - 'baseUrl' => $attrUrlBase - )); ?> -
    -
    -
    -
    - - - - \ No newline at end of file diff --git a/View/GrouperGroups/emaillistsmanage.ctp b/View/GrouperGroups/emaillistsmanage.ctp deleted file mode 100644 index 3c3c41c..0000000 --- a/View/GrouperGroups/emaillistsmanage.ctp +++ /dev/null @@ -1,48 +0,0 @@ -extend('/GrouperGroups/base'); ?> -Html->addCrumb('Email lists I manage'); ?> -element('Components/navigation-emaillists', array('active' => 'emaillistsmanage', 'create' => true)); ?> -element('Components/search', array('active' => 'emaillistsmanaged')); ?> -
    - - - - - - - - - - - - - - - - - - - -
    Html->link( - isset($group['name']) ? $group['domain'] . ':' . $group['name'] : "--", - array( - 'controller' => 'groupergroups', - 'action' => 'emaillistinfo', - '?' => array('groupname' => urlencode($group['name'])) - ) - ) ?> - (10) | - (2) | - (1) - - - -   - - */ ?> - - -
    -
    \ No newline at end of file diff --git a/View/GrouperGroups/emaillistsmember.ctp b/View/GrouperGroups/emaillistsmember.ctp deleted file mode 100644 index ed1264a..0000000 --- a/View/GrouperGroups/emaillistsmember.ctp +++ /dev/null @@ -1,38 +0,0 @@ -extend('/GrouperGroups/base'); ?> -Html->addCrumb('Email lists I can join'); ?> -element('Components/navigation-emaillists', array('active' => 'emaillistsmember')); ?> -element('Components/search', array('active' => 'emaillistsmember')); ?> - -
    - - - - - - - - - - - - - - - - - -
    Html->link( - isset($group['name']) ? $group['domain'].':'.$group['name'] : "No Name", - array( - 'controller' => 'groupergroups', - 'action' => 'emaillistinfo', - '?' => array('groupname' => urlencode($group['name'])) - ) - ) ?> - element('GrouperLite.Components/optAction', array( - 'member' => $group['optedin'], - 'action' => 'leavegroup', - 'group' => $group['name'] - )) : ''; ?> -
    -
    diff --git a/View/GrouperGroups/emaillistsoptin.ctp b/View/GrouperGroups/emaillistsoptin.ctp deleted file mode 100644 index a980633..0000000 --- a/View/GrouperGroups/emaillistsoptin.ctp +++ /dev/null @@ -1,37 +0,0 @@ -extend('/GrouperGroups/base'); ?> -Html->addCrumb('Email lists I can join'); ?> -element('Components/navigation-emaillists', array('active' => 'emaillistsoptin')); ?> -element('Components/search', array('active' => 'emaillistsoptin')); ?> - -
    - - - - - - - - - - - - - - - - - -
    Html->link( - isset($group['name']) ? $group['domain'] . ':' . $group['name'] : "No Name", - array( - 'controller' => 'groupergroups', - 'action' => 'emaillistinfo', - '?' => array('groupname' => urlencode($group['name'])) - ) - ) ?> - -
    -
    \ No newline at end of file diff --git a/View/GrouperGroups/groupcreate.ctp b/View/GrouperGroups/groupcreate.ctp deleted file mode 100644 index c3bf238..0000000 --- a/View/GrouperGroups/groupcreate.ctp +++ /dev/null @@ -1,35 +0,0 @@ -extend('/GrouperGroups/base'); -$this->Html->addCrumb('Create a Group'); - -$model = $this->name; -?> -
    -
    -
    -
    -
    -
    Stems coming in are: Id=stem
    - -
    Value= Display=
    - - - Form->create(false, array( - 'url' => array('controller' => 'groupergroups', 'action' => 'groupcreate') - )); ?> - plugin)) { - if (file_exists(APP . "Plugin/" . $this->plugin . "/View/" . $model . "/groupfields.inc")) { - include(APP . "Plugin/" . $this->plugin . "/View/" . $model . "/groupfields.inc"); - } elseif (file_exists(LOCAL . "Plugin/" . $this->plugin . "/View/" . $model . "/groupfields.inc")) { - include(LOCAL . "Plugin/" . $this->plugin . "/View/" . $model . "/groupfields.inc"); - } - } else { - include(APP . "View/" . $model . "/groupfields.inc"); - } - ?> - Form->end(); ?> -
    -
    -
    -
    \ No newline at end of file diff --git a/View/GrouperGroups/groupcreatetemplate.ctp b/View/GrouperGroups/groupcreatetemplate.ctp deleted file mode 100644 index c7db590..0000000 --- a/View/GrouperGroups/groupcreatetemplate.ctp +++ /dev/null @@ -1,30 +0,0 @@ -extend('/GrouperGroups/base'); -$this->Html->addCrumb('Create a Templated Group'); - -$model = $this->name; -?> -
    -
    -
    -
    -
    - Form->create(false, array( - 'url' => array('controller' => 'groupergroups', 'action' => 'groupcreatetemplate') - )); ?> - plugin)) { - if (file_exists(APP . "Plugin/" . $this->plugin . "/View/" . $model . "/templatefields.inc")) { - include(APP . "Plugin/" . $this->plugin . "/View/" . $model . "/templatefields.inc"); - } elseif (file_exists(LOCAL . "Plugin/" . $this->plugin . "/View/" . $model . "/templatefields.inc")) { - include(LOCAL . "Plugin/" . $this->plugin . "/View/" . $model . "/templatefields.inc"); - } - } else { - include(APP . "View/" . $model . "/templatefields.inc"); - } - ?> - Form->end(); ?> -
    -
    -
    -
    diff --git a/View/GrouperGroups/groupfields.inc b/View/GrouperGroups/groupfields.inc deleted file mode 100644 index ceb7111..0000000 --- a/View/GrouperGroups/groupfields.inc +++ /dev/null @@ -1,210 +0,0 @@ - - - - - -
    -
    - Form->label(false, _txt('pl.grouperlite.form.group.name.label'), array( - 'for' => 'name', - 'class' => "col-sm-3 col-form-label" - )); ?> -
    - Form->input('name', array( - 'label' => false, - 'class' => 'form-control', - 'id' => 'name' - )); ?> - - - -
    -
    - - Form->label(false, _txt('pl.grouperlite.form.group.stem.label'), array( - 'for' => 'folder', - 'class' => "col-sm-3 col-form-label" - )); ?> -
    - Form->input('grouptemplate', array( - 'label' => false, - 'class' => 'form-control', - 'id' => 'folder', - 'default' => 'sandbox:org:unicon:', - 'readonly' => true - )); ?> - - - -
    -
    - */ ?> -
    - Form->label(false, 'Description:', array( - 'for' => 'descr', - 'class' => "col-sm-3 col-form-label" - )); ?> -
    - Form->textarea('description', array( - 'class' => 'form-control', - 'id' => 'descr', - 'rows' => 5 - )); ?> - - - -
    -
    -
    - : -
    -
    - -
    - Form->checkbox('privileges', array( - 'value' => $OPT, - 'class' => 'form-check-input', - 'id' => 'assign-' . $opt, - 'hiddenField' => false - )); ?> - Form->label(false, _txt('pl.grouperlite.form.group.privs.label.' . $opt), array( - 'for' => 'assign-' . $opt, - 'class' => "form-check-label" - )); ?> -
    - -
    -
    -
    - - echo $this->Form->label(false, _txt('pl.grouperlite.form.group.subs.label'), array( - 'for' => 'subscribers', - 'class' => "col-sm-3 col-form-label" - )); ?> -
    - Form->input('grouptemplate', array( - 'label' => false, - 'class' => 'typeahead form-control', - 'id' => 'name', - 'placeholder' => _txt('pl.grouperlite.form.group.subs.placeholder') - )); ?> - - - -
    -
    -
    */ ?> -
    -
    - Form->button(_txt('pl.grouperlite.form.group.action.save'), array( - 'type' => 'submit', - 'class' => 'btn btn-grouper btn-primary btn-lg btn-raised' - )); ?> -
    -
    - \ No newline at end of file diff --git a/View/GrouperGroups/groupinfo.ctp b/View/GrouperGroups/groupinfo.ctp index c668513..9e74792 100644 --- a/View/GrouperGroups/groupinfo.ctp +++ b/View/GrouperGroups/groupinfo.ctp @@ -1,5 +1,6 @@ + extend('/GrouperGroups/base'); ?> -Html->addCrumb('Group configuration'); ?> +Html->addCrumb(_txt('pl.grouperlite.title.groupinfo')); ?>

    - - - - -   - - +
    @@ -29,7 +24,7 @@ $attrUrlBase = $baseUrl . $path . $attrOperation;
    @@ -48,24 +43,4 @@ $attrUrlBase = $baseUrl . $path . $attrOperation;
    - - - - \ No newline at end of file + \ No newline at end of file diff --git a/View/GrouperGroups/groupmember.ctp b/View/GrouperGroups/groupmember.ctp index 561fa98..7bbee89 100644 --- a/View/GrouperGroups/groupmember.ctp +++ b/View/GrouperGroups/groupmember.ctp @@ -1,55 +1,30 @@ + extend('/GrouperGroups/base'); ?> Html->addCrumb(_txt('pl.grouperlite.nav.memberships')); ?> -element('GrouperLite.Components/navigation-groups', array('active' => 'groupmember')); ?> -element('GrouperLite.Components/search', array('active' => 'groupmember')); ?> -
    - element("pagination", array( - 'goto' => false, - 'limit' => false, - 'numbers' => false, - 'counter' => true, - 'class' => 'counter' - )); ?> - - - - - - - - - - - - - - - - - - - - - - -
    - element('GrouperLite.Components/optAction', array( - 'member' => $group['optedin'], - 'action' => 'leavegroup', - 'group' => $group['name'] - )) : ''; ?> -
    - element("pagination", array( - 'goto' => false, - 'limit' => true, - 'numbers' => true, - 'counter' => false - )); ?> -
    -
    \ No newline at end of file + +element('GrouperLite.Components/vue-table', array( + 'groupData' => json_encode(array( + 'adhoc' => $groupmemberships, + 'working' => $wgmemberships + )), + 'treatAsOwner' => $isuserowner === 'T' ? 'true' : 'false', + 'columns' => json_encode(array( + array('value' => 'name', 'label' => _txt('pl.grouperlite.table.name')), + array('value' => 'role', 'label' => _txt('pl.grouperlite.table.role')), + array('value' => 'description', 'label' => _txt('pl.grouperlite.table.description')), + array('value' => 'action', 'label' => _txt('pl.grouperlite.table.action')), + )), + 'optAction' => "leavegroup", + 'actionUrl' => $this->Html->url([ + 'controller' => 'grouper_groups', 'action' => 'leavegroup' + ]), + 'members' => true, + 'addSubscribers' => false, +)); ?> + +
    +

    +
    + + + \ No newline at end of file diff --git a/View/GrouperGroups/groupoptin.ctp b/View/GrouperGroups/groupoptin.ctp index b972042..713ac04 100644 --- a/View/GrouperGroups/groupoptin.ctp +++ b/View/GrouperGroups/groupoptin.ctp @@ -1,55 +1,26 @@ + extend('/GrouperGroups/base'); ?> Html->addCrumb(_txt('pl.grouperlite.nav.groups-can-join')); ?> element('GrouperLite.Components/navigation-groups', array('active' => 'groupoptin')); ?> element('GrouperLite.Components/search', array('active' => 'groupoptin')); ?> -
    - element("pagination", array( - 'goto' => false, - 'limit' => false, - 'numbers' => false, - 'counter' => true, - 'class' => 'counter' - )); ?> - - - - - - - - - - - - - - - - - - - - - - -
    - element('GrouperLite.Components/optAction', array( - 'member' => $group['member'], - 'action' => 'joingroup', - 'group' => $group['name'] - )); ?> -
    - element("pagination", array( - 'goto' => false, - 'limit' => true, - 'numbers' => true, - 'counter' => false - )); ?> -
    -
    \ No newline at end of file +element('GrouperLite.Components/vue-table', array( + 'groupData' => json_encode(array( + 'adhoc' => $groupoptins, + 'working' => array() + )), + 'treatAsOwner' => false, + 'columns' => json_encode(array( + array('value' => 'name', 'label' => _txt('pl.grouperlite.table.name')), + array('value' => 'description', 'label' => _txt('pl.grouperlite.table.description')), + array('value' => 'action', 'label' => _txt('pl.grouperlite.table.action')), + )), + 'optAction' => "joingroup", + 'actionUrl' => $this->Html->url([ + 'controller' => 'grouper_groups', 'action' => 'joingroup' + ]), +)); ?> + +
    +

    +
    + \ No newline at end of file diff --git a/View/GrouperGroups/groupowner.ctp b/View/GrouperGroups/groupowner.ctp index 593b2b3..8abe4f1 100644 --- a/View/GrouperGroups/groupowner.ctp +++ b/View/GrouperGroups/groupowner.ctp @@ -1,62 +1,37 @@ + extend('/GrouperGroups/base'); ?> Html->addCrumb(_txt('pl.grouperlite.nav.groups-presided')); ?> element('Components/navigation-groups', array('active' => 'groupowner')); ?> element('Components/search', array('active' => 'groupowner')); ?> -
    - element("pagination", array( - 'goto' => false, - 'limit' => false, - 'numbers' => false, - 'counter' => true, - 'class' => 'counter' - )); ?> - - - - - - - - - - - - - - - - - - - - - - - - -
    - - - - -
    - element("pagination", array( - 'goto' => false, - 'limit' => true, - 'numbers' => true, - 'counter' => false - )); ?> -
    -
    \ No newline at end of file + 'name', 'label' => _txt('pl.grouperlite.table.name')), + array('value' => 'role', 'label' => _txt('pl.grouperlite.table.role')), + array('value' => 'description', 'label' => _txt('pl.grouperlite.table.description')), + array('value' => 'status', 'label' => _txt('pl.grouperlite.table.status')), + array('value' => 'action', 'label' => _txt('pl.grouperlite.table.action')), +); + +if ($isuserowner !== 'T') { + array_splice($columns, 1, 1); +} +?> + +element('GrouperLite.Components/vue-table', array( + 'groupData' => json_encode(array( + 'adhoc' => $groupsowners, + 'working' => array() + )), + 'treatAsOwner' => $isuserowner === 'T' ? 'true' : 'false', + 'columns' => json_encode($columns), + 'addSubscribers' => true, + 'members' => true +)); ?> + + + +
    +

    +
    + + \ No newline at end of file diff --git a/View/GrouperGroups/index.ctp b/View/GrouperGroups/index.ctp new file mode 100644 index 0000000..8d28873 --- /dev/null +++ b/View/GrouperGroups/index.ctp @@ -0,0 +1,169 @@ + +extend('/GrouperGroups/base'); ?> +Html->script('GrouperLite.vue-router.js'); ?> +Html->addCrumb(_txt('pl.grouperlite.nav.memberships')); ?> + + +
    + Html->image('GrouperLite.Grouper.jpg', array('class' => 'img-fluid mr-2', 'style' => 'height: 50px')); ?> +

    +
    + + + + + +
    +
    + +
    +
    \ No newline at end of file diff --git a/View/GrouperGroups/templatefields.inc b/View/GrouperGroups/templatefields.inc index 47b7d85..5b4d338 100644 --- a/View/GrouperGroups/templatefields.inc +++ b/View/GrouperGroups/templatefields.inc @@ -2,7 +2,7 @@
    Form->label(false, _txt('pl.grouperlite.form.template.work-group-extension.label'), array( 'for' => 'gsh_input_workingGroupExtension', - 'class' => "col-sm-3 col-form-label" + 'class' => "col-sm-3 col-form-label d-flex align-items-center" )); ?>
    Form->input('gsh_input_workingGroupExtension', array( @@ -17,7 +17,7 @@
    Form->label(false, _txt('pl.grouperlite.form.template.work-group-disp-extension.label'), array( 'for' => 'gsh_input_workingGroupDisplayExtension', - 'class' => "col-sm-3 col-form-label" + 'class' => "col-sm-3 col-form-label d-flex align-items-center" )); ?>
    Form->input('gsh_input_workingGroupDisplayExtension', array( @@ -31,7 +31,7 @@
    Form->label(false, _txt('pl.grouperlite.form.template.work-group-description.label'), array( 'for' => 'gsh_input_workingGroupDescription', - 'class' => "col-sm-3 col-form-label" + 'class' => "col-sm-3 col-form-label d-flex align-items-center" )); ?>
    Form->input('gsh_input_workingGroupDescription', array( @@ -42,20 +42,17 @@ )); ?>
    -
    - Form->label(false, _txt('pl.grouperlite.form.template.enable-email-list.label'), array( - 'for' => 'gsh_input_isSympa', - 'class' => "col-sm-3 col-form-label" - )); ?> +
    +
    Form->input( 'gsh_input_isSympa', array( - 'label' => false, + 'label' => true, 'class' => 'form-check-input', 'legend' => false, 'id' => 'gsh_input_isSympa', - 'before' => '
    ', + 'before' => '
    ', 'separator' => '
    ', 'after' => '
    ', 'options' => array( @@ -67,14 +64,14 @@ ) ); ?>
    -
    -
    - +
    +
    +
    Form->input( 'gsh_input_sympaDomain', array( - 'label' => false, + 'label' => true, 'class' => 'form-check-input', 'legend' => false, 'id' => 'gsh_input_sympaDomain', @@ -89,17 +86,14 @@ ) ); ?>
    -
    -
    - Form->label(false, "Is moderated?", array( - 'for' => 'gsh_input_isSympaModerated', - 'class' => "col-sm-3 col-form-label" - )); ?> + +
    +
    Form->input( 'gsh_input_isSympaModerated', array( - 'label' => false, + 'label' => true, 'class' => 'form-check-input', 'legend' => false, 'id' => 'gsh_input_isSympaModerated', @@ -115,17 +109,14 @@ ) ); ?>
    -
    -
    - Form->label(false, _txt('pl.grouperlite.form.template.is-optin.label'), array( - 'for' => 'gsh_input_isOptin', - 'class' => "col-sm-3 col-form-label" - )); ?> + +
    +
    Form->input( 'gsh_input_isOptin', array( - 'label' => false, + 'label' => true, 'class' => 'form-check-input', 'legend' => false, 'id' => 'gsh_input_isOptin', @@ -141,7 +132,7 @@ ) ); ?>
    -
    + -
    - Form->label(false, _txt('pl.grouperlite.form.template.add-wiki.label'), array( - 'for' => 'gsh_input_isConfluence', - 'class' => "col-sm-3 col-form-label" - )); ?> +
    +
    Form->input( 'gsh_input_isConfluence', array( - 'label' => false, + 'label' => true, 'class' => 'form-check-input', 'legend' => false, 'id' => 'gsh_input_isConfluence', @@ -184,17 +172,14 @@ ) ); ?>
    -
    -
    - Form->label(false, _txt('pl.grouperlite.form.template.add-project.label'), array( - 'for' => 'gsh_input_isJira', - 'class' => "col-sm-3 col-form-label" - )); ?> + +
    +
    Form->input( 'gsh_input_isJira', array( - 'label' => false, + 'label' => true, 'class' => 'form-check-input', 'legend' => false, 'id' => 'gsh_input_isJira', @@ -210,7 +195,7 @@ ) ); ?>
    -
    +
    Form->button(_txt('pl.grouperlite.form.group.action.save'), array( diff --git a/webroot/css/co-grouper-plugin.css b/webroot/css/co-grouper-plugin.css index 108088f..bdc2c78 100644 --- a/webroot/css/co-grouper-plugin.css +++ b/webroot/css/co-grouper-plugin.css @@ -10,9 +10,7 @@ } .btn-sm { - padding: 0.6rem 1rem; - font-size: 1rem; - line-height: 1; + font-size: 0.75rem; border-radius: .2rem; } @@ -20,7 +18,7 @@ a { color: var(--primary); } -#grouper-plugin .btn.btn-primary:hover { +#grouper-plugin .btn.btn-primary:not([disabled]):hover { background-color: black; border-color: black; } @@ -33,38 +31,31 @@ a { margin-right: 2px; } -#grouper-plugin .nav.nav-tabs .nav-item a.nav-link { +#grouper-plugin .nav.nav-tabs .nav-item .nav-link { padding: 0.75rem 2rem; transition: background-color .2s cubic-bezier(.4, 0, .2, 1), color .2s cubic-bezier(.4, 0, .2, 1); border-radius: 0; - ; } -#grouper-plugin .nav.nav-tabs .nav-item a.nav-link:not(.active) { +#grouper-plugin .nav.nav-tabs .nav-item .nav-link:not(.active) { color: white; background: var(--primary); } -#grouper-plugin .nav.nav-tabs .nav-item a.nav-link:not(.active):hover { +#grouper-plugin .nav.nav-tabs .nav-item .nav-link:not(.active):hover { border-color: black; background: black; } -#grouper-plugin .nav.nav-tabs .nav-item a.nav-link:not(.active):focus { +#grouper-plugin .nav.nav-tabs .nav-item .nav-link:not(.active):focus { border-color: transparent; } -#grouper-plugin .table {} - -#grouper-plugin .table thead {} - #grouper-plugin .table thead th { background-color: var(--primary); color: white; } -#grouper-plugin .table thead th.group {} - #grouper-plugin .table thead th.group.name { min-width: 200px; } @@ -73,8 +64,12 @@ a { min-width: 200px; } +#grouper-plugin .table thead th.group.role { + min-width: 5%; +} + #grouper-plugin .table thead th.group.description { - width: auto; + width: 40%; } #grouper-plugin .table thead th.group.action { @@ -101,13 +96,16 @@ a { #grouper-plugin .table thead th, #grouper-plugin .table tbody td { padding: 0.75em; text-align: left; - border-top: none; border-right: 2px solid #fff; border-bottom: 2px solid #fff; font-weight: normal; vertical-align: middle; } +#grouper-plugin .table thead th, #grouper-plugin .table tbody tr:not(:last-child):not(.table-light) td { + border-bottom: 1px solid #ececec !important; +} + #grouper-plugin .form-control, #grouper-plugin .custom-select { border-radius: 0; } @@ -118,7 +116,7 @@ a { } #grouper-plugin .input-group.input-group-search .input-group-append .btn.btn-outline-secondary { - font-size: 1.25rem; + font-size: 1rem; border-color: #ced4da; color: inherit; } @@ -131,6 +129,19 @@ a { #grouper-plugin .btn.btn-link.btn-text { padding: 0; margin: 0; + color: inherit; + text-transform: none; +} + +#grouper-plugin .btn.btn-link.btn-text { + padding: 0; + margin: 0; + color: inherit; + text-transform: none; +} + +#grouper-plugin .btn.btn-link.btn-text.h6 { + font-size: 1rem; } #grouper-plugin .paging { @@ -214,6 +225,7 @@ a.list-group-item-action:hover .fa { background-color: var(--primary); color: white; padding: 2px; + border-radius: 0; } .grouper .counter { @@ -221,25 +233,36 @@ a.list-group-item-action:hover .fa { font-size: 0.9rem; } -.grouper .pagination a { - color: white; +.grouper .pagination .pagination-item { + border-right: 1px solid rgba(255, 255, 255, 0.5); } .grouper .pagination .pagination-element:not(.pagination-numbers):not(.pagination-limit) { - padding: 0.5rem; margin-right: 0.5rem; } -.grouper .pagination .pagination-element.pagination-limit { - padding: 0 1rem; +.grouper .pagination .pagination-item-list { + display: flex; } -.grouper .pagination .pagination-element.pagination-numbers { - margin-right: 1rem; +.grouper .pagination .pagination-item .pagination-item-btn { + padding: 0.5rem 1rem; + color: white; + border: none; + background: none; +} + +.grouper .pagination .pagination-item .pagination-item-btn:hover { + background: rgba(255, 255, 255, 0.1); } -.grouper .pagination .pagination-element.pagination-numbers-link:not(:first-child) { - border-left: 1px solid rgba(255, 255, 255, 0.5); +.grouper .pagination .pagination-item.pagination-item-list .pagination-item-btn.current { + background: white; + color: var(--primary); +} + +.grouper .pagination .pagination-element.pagination-numbers { + margin-right: 1rem; } .grouper .pagination .pagination-element.pagination-numbers .pagination-numbers-list>span { @@ -252,13 +275,17 @@ a.list-group-item-action:hover .fa { padding: 0.5rem 1rem; } -.grouper .pagination .pagination-element.pagination-numbers .pagination-numbers-list .current.pagination-numbers-item { - background: white; - color: var(--primary); +.grouper .pagination .pagination-element .pagination-form { + padding: 0rem 0.5rem; + display: flex; + align-items: center; +} +.grouper .pagination .pagination-element .pagination-form * { + margin-left: 0.25rem; } -.grouper .pagination .pagination-element.pagination-form * { - margin: 0 0.5rem 0 0; +.grouper .pagination .pagination-element .pagination-form select { + padding: 0.5rem 0.5rem; } .grouper .pagination .btn { @@ -272,4 +299,20 @@ a.list-group-item-action:hover .fa { .grouper .pagination .muted { color: rgba(255, 255, 255, 0.8); +} + +.radio .form-check-inline label { + margin-bottom: 0; +} + +.table .collapsing { + -webkit-transition: none; + transition: none; + display: none; +} + +.grouper .table .table-light>td { + background-color: #ececec; + border-bottom: 1px solid #FFF !important; + margin-bottom: 1px; } \ No newline at end of file diff --git a/webroot/css/empty b/webroot/css/empty deleted file mode 100644 index e69de29..0000000 diff --git a/webroot/files/groupmember.json b/webroot/files/groupmember.json new file mode 100644 index 0000000..2d50459 --- /dev/null +++ b/webroot/files/groupmember.json @@ -0,0 +1,207 @@ +{ + "adhoc": [ + { + "extension": "admins", + "displayName": "app:Confluence:UniconTestAxel123:admins", + "description": "Admins of confluence space for working group. UniconTestAxel123", + "uuid": "1a0cc0cde33b4556b0565aa1f9c11f7a", + "enabled": "T", + "displayExtension": "admins", + "name": "app:Confluence:UniconTestAxel123:admins", + "typeOfGroup": "group", + "idIndex": "19473", + "optOut": false, + "friendlyName": "admins" + }, + { + "extension": "subscribers", + "displayName": "app:sympa:incommon:AdminTestWG:AdminTest WG subscribers", + "description": "Subscribers list receives working group emails. Test Create of a WG by an Admin", + "uuid": "2e421ab17fc94537a6efcdc38580a093", + "enabled": "T", + "displayExtension": "AdminTest WG subscribers", + "name": "app:sympa:incommon:AdminTestWG:subscribers", + "typeOfGroup": "group", + "idIndex": "19403", + "optOut": false, + "friendlyName": "AdminTest WG subscribers" + }, + { + "extension": "subscribers", + "displayName": "app:sympa:incommon:Bills Group:subscribers", + "description": "Subscribers list receives working group emails. Bills Working Group for testing and splunking", + "uuid": "a564f37496524696b486cffbddabd09d", + "enabled": "T", + "displayExtension": "subscribers", + "name": "app:sympa:incommon:BillsGroup:subscribers", + "typeOfGroup": "group", + "idIndex": "19123", + "optOut": false, + "friendlyName": "subscribers" + }, + { + "extension": "subscribers", + "displayName": "app:sympa:incommon:incommon-comanage:subscribers", + "uuid": "108785997dd7427ca06d5cdb0991cbad", + "enabled": "T", + "displayExtension": "subscribers", + "name": "app:sympa:incommon:incommon-comanage:subscribers", + "typeOfGroup": "group", + "idIndex": "19995", + "optOut": false, + "friendlyName": "subscribers" + }, + { + "extension": "subscriber", + "displayName": "app:sympa:internet2:d_iam_c:Drew's IAM Committee Subscriber", + "description": "Mailing List: Drew's IAM Committee", + "uuid": "3f4c50ee4ce542a6911dfd7cc4191799", + "enabled": "T", + "displayExtension": "Drew's IAM Committee Subscriber", + "name": "app:sympa:internet2:d_iam_c:subscriber", + "typeOfGroup": "group", + "idIndex": "21843", + "optOut": false, + "friendlyName": "Drew's IAM Committee Subscriber" + }, + { + "extension": "users", + "displayName": "ref:InCommon-collab:AdminTestWG:AdminTestWG users", + "description": "This is the AdminTestWG user group", + "uuid": "f092236c89254dc6b7f180706a5c5ad3", + "enabled": "T", + "displayExtension": "AdminTestWG users", + "name": "ref:incommon-collab:AdminTestWG:users", + "typeOfGroup": "group", + "idIndex": "19400", + "optOut": true, + "friendlyName": "AdminTestWG users" + }, + { + "extension": "users", + "displayName": "ref:InCommon-collab:BillsGroup:BillsGroupUsers", + "description": "Bills Group of Users", + "uuid": "215aead1b2a14464a65ce3723f326414", + "enabled": "T", + "displayExtension": "BillsGroupUsers", + "name": "ref:incommon-collab:BillsGroup:users", + "typeOfGroup": "group", + "idIndex": "20042", + "optOut": true, + "friendlyName": "BillsGroupUsers" + }, + { + "extension": "COmanage-UsersGroup-Members", + "displayName": "ref:InCommon-collab:COmanage:COmanage-UsersGroup-Members", + "description": "COmanage Users Group Members", + "uuid": "3ce99f5fc09c42968fae4afe5e63831d", + "enabled": "T", + "displayExtension": "COmanage-UsersGroup-Members", + "name": "ref:incommon-collab:COmanage:COmanage-UsersGroup-Members", + "typeOfGroup": "group", + "idIndex": "19996", + "optOut": true, + "friendlyName": "COmanage-UsersGroup-Members" + }, + { + "extension": "member", + "displayName": "ref:InCommon-collab:Drew's IAM Committee:Drew's IAM Committee Member", + "description": "Drew's IAM Committee member.", + "uuid": "1eafe705651c41fc8e6c50dc753c0891", + "enabled": "T", + "displayExtension": "Drew's IAM Committee Member", + "name": "ref:incommon-collab:d_iam_c:member", + "typeOfGroup": "group", + "idIndex": "20536", + "optOut": true, + "friendlyName": "Drew's IAM Committee Member" + }, + { + "extension": "users", + "displayName": "ref:InCommon-collab:UniconTestAxel333:UniconTestAxel333 users", + "description": "Users role means members of the working group with access to collaboration tools. UniconTestAxel333", + "uuid": "5b84ccacb8a940409f5e126ecabbb9e2", + "enabled": "T", + "displayExtension": "UniconTestAxel333 users", + "name": "ref:incommon-collab:UniconTestAxel333:users", + "typeOfGroup": "group", + "idIndex": "19481", + "optOut": true, + "friendlyName": "UniconTestAxel333 users" + }, + { + "extension": "subscriber", + "displayName": "sandbox:sympa:service:mailing_list:internet2:IAM Fans:IAM Fans Subscriber", + "description": "Mailing list for people who are Identity and Access Mangement fanatics", + "alternateName": "sandbox:sympa:service:mailing_list:internet2:iam_fans:iam_fans_subscriber", + "uuid": "0b23539aa1954f3e8cdd619d3f9e02b6", + "enabled": "T", + "displayExtension": "IAM Fans Subscriber", + "name": "sandbox:sympa:service:mailing_list:internet2:iam_fans:subscriber", + "typeOfGroup": "group", + "idIndex": "19208", + "optOut": true, + "friendlyName": "IAM Fans Subscriber" + }, + { + "extension": "user", + "displayName": "test:OptInOptOut:OptInOptOut User", + "description": "This group can be used in general to be provided privs for opting in and out just for folks in the group vs every entity", + "alternateName": "test:optinOptout", + "uuid": "13bed1e3e58148ec87400c43c9c3c824", + "enabled": "T", + "displayExtension": "OptInOptOut User", + "name": "test:optinoptout:user", + "typeOfGroup": "group", + "idIndex": "19109", + "optOut": true, + "friendlyName": "OptInOptOut User" + } + ], + "working": [ + { + "WGName": "AdminTestWG", + "WGShowName": "AdminTestWG", + "Groups": [ + { + "extension": "users", + "displayName": "app:Confluence:AdminTestWG:users", + "description": "Users of confluence space for working group. Test Create of a WG by an Admin", + "uuid": "caf29f77fdb948eb90a43756f8b3434a", + "enabled": "T", + "displayExtension": "users", + "name": "app:Confluence:AdminTestWG:users", + "typeOfGroup": "group", + "idIndex": "19405", + "optOut": false, + "WGName": "AdminTestWG", + "WGShowName": "AdminTestWG", + "WGRole": "users", + "WGApp": "Confluence" + } + ] + }, + { + "WGName": "BillsGroup", + "WGShowName": "Bills Group", + "Groups": [ + { + "extension": "users", + "displayName": "app:Confluence:Bills Group:users", + "description": "Users of confluence space for working group. Bills Working Group for testing and splunking", + "uuid": "d991a05d5e85494db17e09cc46af7274", + "enabled": "T", + "displayExtension": "users", + "name": "app:Confluence:BillsGroup:users", + "typeOfGroup": "group", + "idIndex": "19125", + "optOut": false, + "WGName": "BillsGroup", + "WGShowName": "Bills Group", + "WGRole": "users", + "WGApp": "Confluence" + } + ] + } + ] +} \ No newline at end of file diff --git a/webroot/files/groupoptin.json b/webroot/files/groupoptin.json new file mode 100644 index 0000000..4ab6f23 --- /dev/null +++ b/webroot/files/groupoptin.json @@ -0,0 +1,14 @@ +[ + { + "extension": "users", + "displayName": "ref:InCommon-collab:UniconTestAxel123:UniconTestAxel123 users", + "description": "Users role means members of the working group with access to collaboration tools. UniconTestAxel123", + "uuid": "ac1641e827894283a8b7259a6549ce5c", + "enabled": "T", + "displayExtension": "UniconTestAxel123 users", + "name": "ref:incommon-collab:UniconTestAxel123:users", + "typeOfGroup": "group", + "idIndex": "19469", + "friendlyName": "UniconTestAxel123 users" + } +] \ No newline at end of file diff --git a/webroot/files/groupowner.json b/webroot/files/groupowner.json new file mode 100644 index 0000000..cc2f4b7 --- /dev/null +++ b/webroot/files/groupowner.json @@ -0,0 +1,26 @@ +[ + { + "extension": "admins", + "displayName": "app:Confluence:UniconTestAxel123:admins", + "description": "Admins of confluence space for working group. UniconTestAxel123", + "uuid": "1a0cc0cde33b4556b0565aa1f9c11f7a", + "enabled": "T", + "displayExtension": "admins", + "name": "app:Confluence:UniconTestAxel123:admins", + "typeOfGroup": "group", + "idIndex": "19473", + "friendlyName": "admins" + }, + { + "extension": "users", + "displayName": "app:Confluence:UniconTestAxel123:users", + "description": "Users of confluence space for working group. UniconTestAxel123", + "uuid": "43b9782ef93f47afa4d75d9911248720", + "enabled": "T", + "displayExtension": "users", + "name": "app:Confluence:UniconTestAxel123:users", + "typeOfGroup": "group", + "idIndex": "19474", + "friendlyName": "users" + } +] \ No newline at end of file diff --git a/webroot/js/autocomplete.js b/webroot/js/autocomplete.js new file mode 100644 index 0000000..284ff57 --- /dev/null +++ b/webroot/js/autocomplete.js @@ -0,0 +1,53 @@ +// ns.testeradmin@at.internet2.edu + +export default { + props: { + group: String + }, + inject: ['txt', 'api'], + data() { + return { + search: '', + val: '', + item: null, + }; + }, + methods: { + addUser(search) { + this.$emit('add', this.item); + } + }, + mounted(el) { + const input = $(this.$el).find('#add-user-input'); + input.autocomplete({ + source: `${this.api.find}?co=${this.api.co}&mode=${this.api.mode}`, + minLength: 1, + maxShowItems: 10, + focus: function( event, ui ) { + this.search = ui.item.label; + this.val = ui.item.identifier; + this.item = ui.item; + return false; + }, + select: (event, ui) => { + this.val = ui.item.identifier; + this.search = ui.item.label; + this.item = ui.item; + $("#addUserbutton").prop('disabled', false).focus(); + return false; + }, + }).autocomplete( "instance" )._renderItem = formatCoPersonAutoselectItem; + }, + template: /*html*/` +
    + + +
    + +
    +
    + ` +} \ No newline at end of file diff --git a/webroot/js/bootstrap.bundle.js b/webroot/js/bootstrap.bundle.js deleted file mode 100644 index 5fda309..0000000 --- a/webroot/js/bootstrap.bundle.js +++ /dev/null @@ -1,7031 +0,0 @@ -/*! - * Bootstrap v4.5.3 (https://getbootstrap.com/) - * Copyright 2011-2020 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('jquery')) : - typeof define === 'function' && define.amd ? define(['exports', 'jquery'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.bootstrap = {}, global.jQuery)); -}(this, (function (exports, $) { 'use strict'; - - function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } - - var $__default = /*#__PURE__*/_interopDefaultLegacy($); - - function _defineProperties(target, props) { - for (var i = 0; i < props.length; i++) { - var descriptor = props[i]; - descriptor.enumerable = descriptor.enumerable || false; - descriptor.configurable = true; - if ("value" in descriptor) descriptor.writable = true; - Object.defineProperty(target, descriptor.key, descriptor); - } - } - - function _createClass(Constructor, protoProps, staticProps) { - if (protoProps) _defineProperties(Constructor.prototype, protoProps); - if (staticProps) _defineProperties(Constructor, staticProps); - return Constructor; - } - - function _extends() { - _extends = Object.assign || function (target) { - for (var i = 1; i < arguments.length; i++) { - var source = arguments[i]; - - for (var key in source) { - if (Object.prototype.hasOwnProperty.call(source, key)) { - target[key] = source[key]; - } - } - } - - return target; - }; - - return _extends.apply(this, arguments); - } - - function _inheritsLoose(subClass, superClass) { - subClass.prototype = Object.create(superClass.prototype); - subClass.prototype.constructor = subClass; - subClass.__proto__ = superClass; - } - - /** - * -------------------------------------------------------------------------- - * Bootstrap (v4.5.3): util.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - /** - * ------------------------------------------------------------------------ - * Private TransitionEnd Helpers - * ------------------------------------------------------------------------ - */ - - var TRANSITION_END = 'transitionend'; - var MAX_UID = 1000000; - var MILLISECONDS_MULTIPLIER = 1000; // Shoutout AngusCroll (https://goo.gl/pxwQGp) - - function toType(obj) { - if (obj === null || typeof obj === 'undefined') { - return "" + obj; - } - - return {}.toString.call(obj).match(/\s([a-z]+)/i)[1].toLowerCase(); - } - - function getSpecialTransitionEndEvent() { - return { - bindType: TRANSITION_END, - delegateType: TRANSITION_END, - handle: function handle(event) { - if ($__default['default'](event.target).is(this)) { - return event.handleObj.handler.apply(this, arguments); // eslint-disable-line prefer-rest-params - } - - return undefined; - } - }; - } - - function transitionEndEmulator(duration) { - var _this = this; - - var called = false; - $__default['default'](this).one(Util.TRANSITION_END, function () { - called = true; - }); - setTimeout(function () { - if (!called) { - Util.triggerTransitionEnd(_this); - } - }, duration); - return this; - } - - function setTransitionEndSupport() { - $__default['default'].fn.emulateTransitionEnd = transitionEndEmulator; - $__default['default'].event.special[Util.TRANSITION_END] = getSpecialTransitionEndEvent(); - } - /** - * -------------------------------------------------------------------------- - * Public Util Api - * -------------------------------------------------------------------------- - */ - - - var Util = { - TRANSITION_END: 'bsTransitionEnd', - getUID: function getUID(prefix) { - do { - prefix += ~~(Math.random() * MAX_UID); // "~~" acts like a faster Math.floor() here - } while (document.getElementById(prefix)); - - return prefix; - }, - getSelectorFromElement: function getSelectorFromElement(element) { - var selector = element.getAttribute('data-target'); - - if (!selector || selector === '#') { - var hrefAttr = element.getAttribute('href'); - selector = hrefAttr && hrefAttr !== '#' ? hrefAttr.trim() : ''; - } - - try { - return document.querySelector(selector) ? selector : null; - } catch (_) { - return null; - } - }, - getTransitionDurationFromElement: function getTransitionDurationFromElement(element) { - if (!element) { - return 0; - } // Get transition-duration of the element - - - var transitionDuration = $__default['default'](element).css('transition-duration'); - var transitionDelay = $__default['default'](element).css('transition-delay'); - var floatTransitionDuration = parseFloat(transitionDuration); - var floatTransitionDelay = parseFloat(transitionDelay); // Return 0 if element or transition duration is not found - - if (!floatTransitionDuration && !floatTransitionDelay) { - return 0; - } // If multiple durations are defined, take the first - - - transitionDuration = transitionDuration.split(',')[0]; - transitionDelay = transitionDelay.split(',')[0]; - return (parseFloat(transitionDuration) + parseFloat(transitionDelay)) * MILLISECONDS_MULTIPLIER; - }, - reflow: function reflow(element) { - return element.offsetHeight; - }, - triggerTransitionEnd: function triggerTransitionEnd(element) { - $__default['default'](element).trigger(TRANSITION_END); - }, - supportsTransitionEnd: function supportsTransitionEnd() { - return Boolean(TRANSITION_END); - }, - isElement: function isElement(obj) { - return (obj[0] || obj).nodeType; - }, - typeCheckConfig: function typeCheckConfig(componentName, config, configTypes) { - for (var property in configTypes) { - if (Object.prototype.hasOwnProperty.call(configTypes, property)) { - var expectedTypes = configTypes[property]; - var value = config[property]; - var valueType = value && Util.isElement(value) ? 'element' : toType(value); - - if (!new RegExp(expectedTypes).test(valueType)) { - throw new Error(componentName.toUpperCase() + ": " + ("Option \"" + property + "\" provided type \"" + valueType + "\" ") + ("but expected type \"" + expectedTypes + "\".")); - } - } - } - }, - findShadowRoot: function findShadowRoot(element) { - if (!document.documentElement.attachShadow) { - return null; - } // Can find the shadow root otherwise it'll return the document - - - if (typeof element.getRootNode === 'function') { - var root = element.getRootNode(); - return root instanceof ShadowRoot ? root : null; - } - - if (element instanceof ShadowRoot) { - return element; - } // when we don't find a shadow root - - - if (!element.parentNode) { - return null; - } - - return Util.findShadowRoot(element.parentNode); - }, - jQueryDetection: function jQueryDetection() { - if (typeof $__default['default'] === 'undefined') { - throw new TypeError('Bootstrap\'s JavaScript requires jQuery. jQuery must be included before Bootstrap\'s JavaScript.'); - } - - var version = $__default['default'].fn.jquery.split(' ')[0].split('.'); - var minMajor = 1; - var ltMajor = 2; - var minMinor = 9; - var minPatch = 1; - var maxMajor = 4; - - if (version[0] < ltMajor && version[1] < minMinor || version[0] === minMajor && version[1] === minMinor && version[2] < minPatch || version[0] >= maxMajor) { - throw new Error('Bootstrap\'s JavaScript requires at least jQuery v1.9.1 but less than v4.0.0'); - } - } - }; - Util.jQueryDetection(); - setTransitionEndSupport(); - - /** - * ------------------------------------------------------------------------ - * Constants - * ------------------------------------------------------------------------ - */ - - var NAME = 'alert'; - var VERSION = '4.5.3'; - var DATA_KEY = 'bs.alert'; - var EVENT_KEY = "." + DATA_KEY; - var DATA_API_KEY = '.data-api'; - var JQUERY_NO_CONFLICT = $__default['default'].fn[NAME]; - var SELECTOR_DISMISS = '[data-dismiss="alert"]'; - var EVENT_CLOSE = "close" + EVENT_KEY; - var EVENT_CLOSED = "closed" + EVENT_KEY; - var EVENT_CLICK_DATA_API = "click" + EVENT_KEY + DATA_API_KEY; - var CLASS_NAME_ALERT = 'alert'; - var CLASS_NAME_FADE = 'fade'; - var CLASS_NAME_SHOW = 'show'; - /** - * ------------------------------------------------------------------------ - * Class Definition - * ------------------------------------------------------------------------ - */ - - var Alert = /*#__PURE__*/function () { - function Alert(element) { - this._element = element; - } // Getters - - - var _proto = Alert.prototype; - - // Public - _proto.close = function close(element) { - var rootElement = this._element; - - if (element) { - rootElement = this._getRootElement(element); - } - - var customEvent = this._triggerCloseEvent(rootElement); - - if (customEvent.isDefaultPrevented()) { - return; - } - - this._removeElement(rootElement); - }; - - _proto.dispose = function dispose() { - $__default['default'].removeData(this._element, DATA_KEY); - this._element = null; - } // Private - ; - - _proto._getRootElement = function _getRootElement(element) { - var selector = Util.getSelectorFromElement(element); - var parent = false; - - if (selector) { - parent = document.querySelector(selector); - } - - if (!parent) { - parent = $__default['default'](element).closest("." + CLASS_NAME_ALERT)[0]; - } - - return parent; - }; - - _proto._triggerCloseEvent = function _triggerCloseEvent(element) { - var closeEvent = $__default['default'].Event(EVENT_CLOSE); - $__default['default'](element).trigger(closeEvent); - return closeEvent; - }; - - _proto._removeElement = function _removeElement(element) { - var _this = this; - - $__default['default'](element).removeClass(CLASS_NAME_SHOW); - - if (!$__default['default'](element).hasClass(CLASS_NAME_FADE)) { - this._destroyElement(element); - - return; - } - - var transitionDuration = Util.getTransitionDurationFromElement(element); - $__default['default'](element).one(Util.TRANSITION_END, function (event) { - return _this._destroyElement(element, event); - }).emulateTransitionEnd(transitionDuration); - }; - - _proto._destroyElement = function _destroyElement(element) { - $__default['default'](element).detach().trigger(EVENT_CLOSED).remove(); - } // Static - ; - - Alert._jQueryInterface = function _jQueryInterface(config) { - return this.each(function () { - var $element = $__default['default'](this); - var data = $element.data(DATA_KEY); - - if (!data) { - data = new Alert(this); - $element.data(DATA_KEY, data); - } - - if (config === 'close') { - data[config](this); - } - }); - }; - - Alert._handleDismiss = function _handleDismiss(alertInstance) { - return function (event) { - if (event) { - event.preventDefault(); - } - - alertInstance.close(this); - }; - }; - - _createClass(Alert, null, [{ - key: "VERSION", - get: function get() { - return VERSION; - } - }]); - - return Alert; - }(); - /** - * ------------------------------------------------------------------------ - * Data Api implementation - * ------------------------------------------------------------------------ - */ - - - $__default['default'](document).on(EVENT_CLICK_DATA_API, SELECTOR_DISMISS, Alert._handleDismiss(new Alert())); - /** - * ------------------------------------------------------------------------ - * jQuery - * ------------------------------------------------------------------------ - */ - - $__default['default'].fn[NAME] = Alert._jQueryInterface; - $__default['default'].fn[NAME].Constructor = Alert; - - $__default['default'].fn[NAME].noConflict = function () { - $__default['default'].fn[NAME] = JQUERY_NO_CONFLICT; - return Alert._jQueryInterface; - }; - - /** - * ------------------------------------------------------------------------ - * Constants - * ------------------------------------------------------------------------ - */ - - var NAME$1 = 'button'; - var VERSION$1 = '4.5.3'; - var DATA_KEY$1 = 'bs.button'; - var EVENT_KEY$1 = "." + DATA_KEY$1; - var DATA_API_KEY$1 = '.data-api'; - var JQUERY_NO_CONFLICT$1 = $__default['default'].fn[NAME$1]; - var CLASS_NAME_ACTIVE = 'active'; - var CLASS_NAME_BUTTON = 'btn'; - var CLASS_NAME_FOCUS = 'focus'; - var SELECTOR_DATA_TOGGLE_CARROT = '[data-toggle^="button"]'; - var SELECTOR_DATA_TOGGLES = '[data-toggle="buttons"]'; - var SELECTOR_DATA_TOGGLE = '[data-toggle="button"]'; - var SELECTOR_DATA_TOGGLES_BUTTONS = '[data-toggle="buttons"] .btn'; - var SELECTOR_INPUT = 'input:not([type="hidden"])'; - var SELECTOR_ACTIVE = '.active'; - var SELECTOR_BUTTON = '.btn'; - var EVENT_CLICK_DATA_API$1 = "click" + EVENT_KEY$1 + DATA_API_KEY$1; - var EVENT_FOCUS_BLUR_DATA_API = "focus" + EVENT_KEY$1 + DATA_API_KEY$1 + " " + ("blur" + EVENT_KEY$1 + DATA_API_KEY$1); - var EVENT_LOAD_DATA_API = "load" + EVENT_KEY$1 + DATA_API_KEY$1; - /** - * ------------------------------------------------------------------------ - * Class Definition - * ------------------------------------------------------------------------ - */ - - var Button = /*#__PURE__*/function () { - function Button(element) { - this._element = element; - this.shouldAvoidTriggerChange = false; - } // Getters - - - var _proto = Button.prototype; - - // Public - _proto.toggle = function toggle() { - var triggerChangeEvent = true; - var addAriaPressed = true; - var rootElement = $__default['default'](this._element).closest(SELECTOR_DATA_TOGGLES)[0]; - - if (rootElement) { - var input = this._element.querySelector(SELECTOR_INPUT); - - if (input) { - if (input.type === 'radio') { - if (input.checked && this._element.classList.contains(CLASS_NAME_ACTIVE)) { - triggerChangeEvent = false; - } else { - var activeElement = rootElement.querySelector(SELECTOR_ACTIVE); - - if (activeElement) { - $__default['default'](activeElement).removeClass(CLASS_NAME_ACTIVE); - } - } - } - - if (triggerChangeEvent) { - // if it's not a radio button or checkbox don't add a pointless/invalid checked property to the input - if (input.type === 'checkbox' || input.type === 'radio') { - input.checked = !this._element.classList.contains(CLASS_NAME_ACTIVE); - } - - if (!this.shouldAvoidTriggerChange) { - $__default['default'](input).trigger('change'); - } - } - - input.focus(); - addAriaPressed = false; - } - } - - if (!(this._element.hasAttribute('disabled') || this._element.classList.contains('disabled'))) { - if (addAriaPressed) { - this._element.setAttribute('aria-pressed', !this._element.classList.contains(CLASS_NAME_ACTIVE)); - } - - if (triggerChangeEvent) { - $__default['default'](this._element).toggleClass(CLASS_NAME_ACTIVE); - } - } - }; - - _proto.dispose = function dispose() { - $__default['default'].removeData(this._element, DATA_KEY$1); - this._element = null; - } // Static - ; - - Button._jQueryInterface = function _jQueryInterface(config, avoidTriggerChange) { - return this.each(function () { - var $element = $__default['default'](this); - var data = $element.data(DATA_KEY$1); - - if (!data) { - data = new Button(this); - $element.data(DATA_KEY$1, data); - } - - data.shouldAvoidTriggerChange = avoidTriggerChange; - - if (config === 'toggle') { - data[config](); - } - }); - }; - - _createClass(Button, null, [{ - key: "VERSION", - get: function get() { - return VERSION$1; - } - }]); - - return Button; - }(); - /** - * ------------------------------------------------------------------------ - * Data Api implementation - * ------------------------------------------------------------------------ - */ - - - $__default['default'](document).on(EVENT_CLICK_DATA_API$1, SELECTOR_DATA_TOGGLE_CARROT, function (event) { - var button = event.target; - var initialButton = button; - - if (!$__default['default'](button).hasClass(CLASS_NAME_BUTTON)) { - button = $__default['default'](button).closest(SELECTOR_BUTTON)[0]; - } - - if (!button || button.hasAttribute('disabled') || button.classList.contains('disabled')) { - event.preventDefault(); // work around Firefox bug #1540995 - } else { - var inputBtn = button.querySelector(SELECTOR_INPUT); - - if (inputBtn && (inputBtn.hasAttribute('disabled') || inputBtn.classList.contains('disabled'))) { - event.preventDefault(); // work around Firefox bug #1540995 - - return; - } - - if (initialButton.tagName === 'INPUT' || button.tagName !== 'LABEL') { - Button._jQueryInterface.call($__default['default'](button), 'toggle', initialButton.tagName === 'INPUT'); - } - } - }).on(EVENT_FOCUS_BLUR_DATA_API, SELECTOR_DATA_TOGGLE_CARROT, function (event) { - var button = $__default['default'](event.target).closest(SELECTOR_BUTTON)[0]; - $__default['default'](button).toggleClass(CLASS_NAME_FOCUS, /^focus(in)?$/.test(event.type)); - }); - $__default['default'](window).on(EVENT_LOAD_DATA_API, function () { - // ensure correct active class is set to match the controls' actual values/states - // find all checkboxes/readio buttons inside data-toggle groups - var buttons = [].slice.call(document.querySelectorAll(SELECTOR_DATA_TOGGLES_BUTTONS)); - - for (var i = 0, len = buttons.length; i < len; i++) { - var button = buttons[i]; - var input = button.querySelector(SELECTOR_INPUT); - - if (input.checked || input.hasAttribute('checked')) { - button.classList.add(CLASS_NAME_ACTIVE); - } else { - button.classList.remove(CLASS_NAME_ACTIVE); - } - } // find all button toggles - - - buttons = [].slice.call(document.querySelectorAll(SELECTOR_DATA_TOGGLE)); - - for (var _i = 0, _len = buttons.length; _i < _len; _i++) { - var _button = buttons[_i]; - - if (_button.getAttribute('aria-pressed') === 'true') { - _button.classList.add(CLASS_NAME_ACTIVE); - } else { - _button.classList.remove(CLASS_NAME_ACTIVE); - } - } - }); - /** - * ------------------------------------------------------------------------ - * jQuery - * ------------------------------------------------------------------------ - */ - - $__default['default'].fn[NAME$1] = Button._jQueryInterface; - $__default['default'].fn[NAME$1].Constructor = Button; - - $__default['default'].fn[NAME$1].noConflict = function () { - $__default['default'].fn[NAME$1] = JQUERY_NO_CONFLICT$1; - return Button._jQueryInterface; - }; - - /** - * ------------------------------------------------------------------------ - * Constants - * ------------------------------------------------------------------------ - */ - - var NAME$2 = 'carousel'; - var VERSION$2 = '4.5.3'; - var DATA_KEY$2 = 'bs.carousel'; - var EVENT_KEY$2 = "." + DATA_KEY$2; - var DATA_API_KEY$2 = '.data-api'; - var JQUERY_NO_CONFLICT$2 = $__default['default'].fn[NAME$2]; - var ARROW_LEFT_KEYCODE = 37; // KeyboardEvent.which value for left arrow key - - var ARROW_RIGHT_KEYCODE = 39; // KeyboardEvent.which value for right arrow key - - var TOUCHEVENT_COMPAT_WAIT = 500; // Time for mouse compat events to fire after touch - - var SWIPE_THRESHOLD = 40; - var Default = { - interval: 5000, - keyboard: true, - slide: false, - pause: 'hover', - wrap: true, - touch: true - }; - var DefaultType = { - interval: '(number|boolean)', - keyboard: 'boolean', - slide: '(boolean|string)', - pause: '(string|boolean)', - wrap: 'boolean', - touch: 'boolean' - }; - var DIRECTION_NEXT = 'next'; - var DIRECTION_PREV = 'prev'; - var DIRECTION_LEFT = 'left'; - var DIRECTION_RIGHT = 'right'; - var EVENT_SLIDE = "slide" + EVENT_KEY$2; - var EVENT_SLID = "slid" + EVENT_KEY$2; - var EVENT_KEYDOWN = "keydown" + EVENT_KEY$2; - var EVENT_MOUSEENTER = "mouseenter" + EVENT_KEY$2; - var EVENT_MOUSELEAVE = "mouseleave" + EVENT_KEY$2; - var EVENT_TOUCHSTART = "touchstart" + EVENT_KEY$2; - var EVENT_TOUCHMOVE = "touchmove" + EVENT_KEY$2; - var EVENT_TOUCHEND = "touchend" + EVENT_KEY$2; - var EVENT_POINTERDOWN = "pointerdown" + EVENT_KEY$2; - var EVENT_POINTERUP = "pointerup" + EVENT_KEY$2; - var EVENT_DRAG_START = "dragstart" + EVENT_KEY$2; - var EVENT_LOAD_DATA_API$1 = "load" + EVENT_KEY$2 + DATA_API_KEY$2; - var EVENT_CLICK_DATA_API$2 = "click" + EVENT_KEY$2 + DATA_API_KEY$2; - var CLASS_NAME_CAROUSEL = 'carousel'; - var CLASS_NAME_ACTIVE$1 = 'active'; - var CLASS_NAME_SLIDE = 'slide'; - var CLASS_NAME_RIGHT = 'carousel-item-right'; - var CLASS_NAME_LEFT = 'carousel-item-left'; - var CLASS_NAME_NEXT = 'carousel-item-next'; - var CLASS_NAME_PREV = 'carousel-item-prev'; - var CLASS_NAME_POINTER_EVENT = 'pointer-event'; - var SELECTOR_ACTIVE$1 = '.active'; - var SELECTOR_ACTIVE_ITEM = '.active.carousel-item'; - var SELECTOR_ITEM = '.carousel-item'; - var SELECTOR_ITEM_IMG = '.carousel-item img'; - var SELECTOR_NEXT_PREV = '.carousel-item-next, .carousel-item-prev'; - var SELECTOR_INDICATORS = '.carousel-indicators'; - var SELECTOR_DATA_SLIDE = '[data-slide], [data-slide-to]'; - var SELECTOR_DATA_RIDE = '[data-ride="carousel"]'; - var PointerType = { - TOUCH: 'touch', - PEN: 'pen' - }; - /** - * ------------------------------------------------------------------------ - * Class Definition - * ------------------------------------------------------------------------ - */ - - var Carousel = /*#__PURE__*/function () { - function Carousel(element, config) { - this._items = null; - this._interval = null; - this._activeElement = null; - this._isPaused = false; - this._isSliding = false; - this.touchTimeout = null; - this.touchStartX = 0; - this.touchDeltaX = 0; - this._config = this._getConfig(config); - this._element = element; - this._indicatorsElement = this._element.querySelector(SELECTOR_INDICATORS); - this._touchSupported = 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0; - this._pointerEvent = Boolean(window.PointerEvent || window.MSPointerEvent); - - this._addEventListeners(); - } // Getters - - - var _proto = Carousel.prototype; - - // Public - _proto.next = function next() { - if (!this._isSliding) { - this._slide(DIRECTION_NEXT); - } - }; - - _proto.nextWhenVisible = function nextWhenVisible() { - var $element = $__default['default'](this._element); // Don't call next when the page isn't visible - // or the carousel or its parent isn't visible - - if (!document.hidden && $element.is(':visible') && $element.css('visibility') !== 'hidden') { - this.next(); - } - }; - - _proto.prev = function prev() { - if (!this._isSliding) { - this._slide(DIRECTION_PREV); - } - }; - - _proto.pause = function pause(event) { - if (!event) { - this._isPaused = true; - } - - if (this._element.querySelector(SELECTOR_NEXT_PREV)) { - Util.triggerTransitionEnd(this._element); - this.cycle(true); - } - - clearInterval(this._interval); - this._interval = null; - }; - - _proto.cycle = function cycle(event) { - if (!event) { - this._isPaused = false; - } - - if (this._interval) { - clearInterval(this._interval); - this._interval = null; - } - - if (this._config.interval && !this._isPaused) { - this._interval = setInterval((document.visibilityState ? this.nextWhenVisible : this.next).bind(this), this._config.interval); - } - }; - - _proto.to = function to(index) { - var _this = this; - - this._activeElement = this._element.querySelector(SELECTOR_ACTIVE_ITEM); - - var activeIndex = this._getItemIndex(this._activeElement); - - if (index > this._items.length - 1 || index < 0) { - return; - } - - if (this._isSliding) { - $__default['default'](this._element).one(EVENT_SLID, function () { - return _this.to(index); - }); - return; - } - - if (activeIndex === index) { - this.pause(); - this.cycle(); - return; - } - - var direction = index > activeIndex ? DIRECTION_NEXT : DIRECTION_PREV; - - this._slide(direction, this._items[index]); - }; - - _proto.dispose = function dispose() { - $__default['default'](this._element).off(EVENT_KEY$2); - $__default['default'].removeData(this._element, DATA_KEY$2); - this._items = null; - this._config = null; - this._element = null; - this._interval = null; - this._isPaused = null; - this._isSliding = null; - this._activeElement = null; - this._indicatorsElement = null; - } // Private - ; - - _proto._getConfig = function _getConfig(config) { - config = _extends({}, Default, config); - Util.typeCheckConfig(NAME$2, config, DefaultType); - return config; - }; - - _proto._handleSwipe = function _handleSwipe() { - var absDeltax = Math.abs(this.touchDeltaX); - - if (absDeltax <= SWIPE_THRESHOLD) { - return; - } - - var direction = absDeltax / this.touchDeltaX; - this.touchDeltaX = 0; // swipe left - - if (direction > 0) { - this.prev(); - } // swipe right - - - if (direction < 0) { - this.next(); - } - }; - - _proto._addEventListeners = function _addEventListeners() { - var _this2 = this; - - if (this._config.keyboard) { - $__default['default'](this._element).on(EVENT_KEYDOWN, function (event) { - return _this2._keydown(event); - }); - } - - if (this._config.pause === 'hover') { - $__default['default'](this._element).on(EVENT_MOUSEENTER, function (event) { - return _this2.pause(event); - }).on(EVENT_MOUSELEAVE, function (event) { - return _this2.cycle(event); - }); - } - - if (this._config.touch) { - this._addTouchEventListeners(); - } - }; - - _proto._addTouchEventListeners = function _addTouchEventListeners() { - var _this3 = this; - - if (!this._touchSupported) { - return; - } - - var start = function start(event) { - if (_this3._pointerEvent && PointerType[event.originalEvent.pointerType.toUpperCase()]) { - _this3.touchStartX = event.originalEvent.clientX; - } else if (!_this3._pointerEvent) { - _this3.touchStartX = event.originalEvent.touches[0].clientX; - } - }; - - var move = function move(event) { - // ensure swiping with one touch and not pinching - if (event.originalEvent.touches && event.originalEvent.touches.length > 1) { - _this3.touchDeltaX = 0; - } else { - _this3.touchDeltaX = event.originalEvent.touches[0].clientX - _this3.touchStartX; - } - }; - - var end = function end(event) { - if (_this3._pointerEvent && PointerType[event.originalEvent.pointerType.toUpperCase()]) { - _this3.touchDeltaX = event.originalEvent.clientX - _this3.touchStartX; - } - - _this3._handleSwipe(); - - if (_this3._config.pause === 'hover') { - // If it's a touch-enabled device, mouseenter/leave are fired as - // part of the mouse compatibility events on first tap - the carousel - // would stop cycling until user tapped out of it; - // here, we listen for touchend, explicitly pause the carousel - // (as if it's the second time we tap on it, mouseenter compat event - // is NOT fired) and after a timeout (to allow for mouse compatibility - // events to fire) we explicitly restart cycling - _this3.pause(); - - if (_this3.touchTimeout) { - clearTimeout(_this3.touchTimeout); - } - - _this3.touchTimeout = setTimeout(function (event) { - return _this3.cycle(event); - }, TOUCHEVENT_COMPAT_WAIT + _this3._config.interval); - } - }; - - $__default['default'](this._element.querySelectorAll(SELECTOR_ITEM_IMG)).on(EVENT_DRAG_START, function (e) { - return e.preventDefault(); - }); - - if (this._pointerEvent) { - $__default['default'](this._element).on(EVENT_POINTERDOWN, function (event) { - return start(event); - }); - $__default['default'](this._element).on(EVENT_POINTERUP, function (event) { - return end(event); - }); - - this._element.classList.add(CLASS_NAME_POINTER_EVENT); - } else { - $__default['default'](this._element).on(EVENT_TOUCHSTART, function (event) { - return start(event); - }); - $__default['default'](this._element).on(EVENT_TOUCHMOVE, function (event) { - return move(event); - }); - $__default['default'](this._element).on(EVENT_TOUCHEND, function (event) { - return end(event); - }); - } - }; - - _proto._keydown = function _keydown(event) { - if (/input|textarea/i.test(event.target.tagName)) { - return; - } - - switch (event.which) { - case ARROW_LEFT_KEYCODE: - event.preventDefault(); - this.prev(); - break; - - case ARROW_RIGHT_KEYCODE: - event.preventDefault(); - this.next(); - break; - } - }; - - _proto._getItemIndex = function _getItemIndex(element) { - this._items = element && element.parentNode ? [].slice.call(element.parentNode.querySelectorAll(SELECTOR_ITEM)) : []; - return this._items.indexOf(element); - }; - - _proto._getItemByDirection = function _getItemByDirection(direction, activeElement) { - var isNextDirection = direction === DIRECTION_NEXT; - var isPrevDirection = direction === DIRECTION_PREV; - - var activeIndex = this._getItemIndex(activeElement); - - var lastItemIndex = this._items.length - 1; - var isGoingToWrap = isPrevDirection && activeIndex === 0 || isNextDirection && activeIndex === lastItemIndex; - - if (isGoingToWrap && !this._config.wrap) { - return activeElement; - } - - var delta = direction === DIRECTION_PREV ? -1 : 1; - var itemIndex = (activeIndex + delta) % this._items.length; - return itemIndex === -1 ? this._items[this._items.length - 1] : this._items[itemIndex]; - }; - - _proto._triggerSlideEvent = function _triggerSlideEvent(relatedTarget, eventDirectionName) { - var targetIndex = this._getItemIndex(relatedTarget); - - var fromIndex = this._getItemIndex(this._element.querySelector(SELECTOR_ACTIVE_ITEM)); - - var slideEvent = $__default['default'].Event(EVENT_SLIDE, { - relatedTarget: relatedTarget, - direction: eventDirectionName, - from: fromIndex, - to: targetIndex - }); - $__default['default'](this._element).trigger(slideEvent); - return slideEvent; - }; - - _proto._setActiveIndicatorElement = function _setActiveIndicatorElement(element) { - if (this._indicatorsElement) { - var indicators = [].slice.call(this._indicatorsElement.querySelectorAll(SELECTOR_ACTIVE$1)); - $__default['default'](indicators).removeClass(CLASS_NAME_ACTIVE$1); - - var nextIndicator = this._indicatorsElement.children[this._getItemIndex(element)]; - - if (nextIndicator) { - $__default['default'](nextIndicator).addClass(CLASS_NAME_ACTIVE$1); - } - } - }; - - _proto._slide = function _slide(direction, element) { - var _this4 = this; - - var activeElement = this._element.querySelector(SELECTOR_ACTIVE_ITEM); - - var activeElementIndex = this._getItemIndex(activeElement); - - var nextElement = element || activeElement && this._getItemByDirection(direction, activeElement); - - var nextElementIndex = this._getItemIndex(nextElement); - - var isCycling = Boolean(this._interval); - var directionalClassName; - var orderClassName; - var eventDirectionName; - - if (direction === DIRECTION_NEXT) { - directionalClassName = CLASS_NAME_LEFT; - orderClassName = CLASS_NAME_NEXT; - eventDirectionName = DIRECTION_LEFT; - } else { - directionalClassName = CLASS_NAME_RIGHT; - orderClassName = CLASS_NAME_PREV; - eventDirectionName = DIRECTION_RIGHT; - } - - if (nextElement && $__default['default'](nextElement).hasClass(CLASS_NAME_ACTIVE$1)) { - this._isSliding = false; - return; - } - - var slideEvent = this._triggerSlideEvent(nextElement, eventDirectionName); - - if (slideEvent.isDefaultPrevented()) { - return; - } - - if (!activeElement || !nextElement) { - // Some weirdness is happening, so we bail - return; - } - - this._isSliding = true; - - if (isCycling) { - this.pause(); - } - - this._setActiveIndicatorElement(nextElement); - - var slidEvent = $__default['default'].Event(EVENT_SLID, { - relatedTarget: nextElement, - direction: eventDirectionName, - from: activeElementIndex, - to: nextElementIndex - }); - - if ($__default['default'](this._element).hasClass(CLASS_NAME_SLIDE)) { - $__default['default'](nextElement).addClass(orderClassName); - Util.reflow(nextElement); - $__default['default'](activeElement).addClass(directionalClassName); - $__default['default'](nextElement).addClass(directionalClassName); - var nextElementInterval = parseInt(nextElement.getAttribute('data-interval'), 10); - - if (nextElementInterval) { - this._config.defaultInterval = this._config.defaultInterval || this._config.interval; - this._config.interval = nextElementInterval; - } else { - this._config.interval = this._config.defaultInterval || this._config.interval; - } - - var transitionDuration = Util.getTransitionDurationFromElement(activeElement); - $__default['default'](activeElement).one(Util.TRANSITION_END, function () { - $__default['default'](nextElement).removeClass(directionalClassName + " " + orderClassName).addClass(CLASS_NAME_ACTIVE$1); - $__default['default'](activeElement).removeClass(CLASS_NAME_ACTIVE$1 + " " + orderClassName + " " + directionalClassName); - _this4._isSliding = false; - setTimeout(function () { - return $__default['default'](_this4._element).trigger(slidEvent); - }, 0); - }).emulateTransitionEnd(transitionDuration); - } else { - $__default['default'](activeElement).removeClass(CLASS_NAME_ACTIVE$1); - $__default['default'](nextElement).addClass(CLASS_NAME_ACTIVE$1); - this._isSliding = false; - $__default['default'](this._element).trigger(slidEvent); - } - - if (isCycling) { - this.cycle(); - } - } // Static - ; - - Carousel._jQueryInterface = function _jQueryInterface(config) { - return this.each(function () { - var data = $__default['default'](this).data(DATA_KEY$2); - - var _config = _extends({}, Default, $__default['default'](this).data()); - - if (typeof config === 'object') { - _config = _extends({}, _config, config); - } - - var action = typeof config === 'string' ? config : _config.slide; - - if (!data) { - data = new Carousel(this, _config); - $__default['default'](this).data(DATA_KEY$2, data); - } - - if (typeof config === 'number') { - data.to(config); - } else if (typeof action === 'string') { - if (typeof data[action] === 'undefined') { - throw new TypeError("No method named \"" + action + "\""); - } - - data[action](); - } else if (_config.interval && _config.ride) { - data.pause(); - data.cycle(); - } - }); - }; - - Carousel._dataApiClickHandler = function _dataApiClickHandler(event) { - var selector = Util.getSelectorFromElement(this); - - if (!selector) { - return; - } - - var target = $__default['default'](selector)[0]; - - if (!target || !$__default['default'](target).hasClass(CLASS_NAME_CAROUSEL)) { - return; - } - - var config = _extends({}, $__default['default'](target).data(), $__default['default'](this).data()); - - var slideIndex = this.getAttribute('data-slide-to'); - - if (slideIndex) { - config.interval = false; - } - - Carousel._jQueryInterface.call($__default['default'](target), config); - - if (slideIndex) { - $__default['default'](target).data(DATA_KEY$2).to(slideIndex); - } - - event.preventDefault(); - }; - - _createClass(Carousel, null, [{ - key: "VERSION", - get: function get() { - return VERSION$2; - } - }, { - key: "Default", - get: function get() { - return Default; - } - }]); - - return Carousel; - }(); - /** - * ------------------------------------------------------------------------ - * Data Api implementation - * ------------------------------------------------------------------------ - */ - - - $__default['default'](document).on(EVENT_CLICK_DATA_API$2, SELECTOR_DATA_SLIDE, Carousel._dataApiClickHandler); - $__default['default'](window).on(EVENT_LOAD_DATA_API$1, function () { - var carousels = [].slice.call(document.querySelectorAll(SELECTOR_DATA_RIDE)); - - for (var i = 0, len = carousels.length; i < len; i++) { - var $carousel = $__default['default'](carousels[i]); - - Carousel._jQueryInterface.call($carousel, $carousel.data()); - } - }); - /** - * ------------------------------------------------------------------------ - * jQuery - * ------------------------------------------------------------------------ - */ - - $__default['default'].fn[NAME$2] = Carousel._jQueryInterface; - $__default['default'].fn[NAME$2].Constructor = Carousel; - - $__default['default'].fn[NAME$2].noConflict = function () { - $__default['default'].fn[NAME$2] = JQUERY_NO_CONFLICT$2; - return Carousel._jQueryInterface; - }; - - /** - * ------------------------------------------------------------------------ - * Constants - * ------------------------------------------------------------------------ - */ - - var NAME$3 = 'collapse'; - var VERSION$3 = '4.5.3'; - var DATA_KEY$3 = 'bs.collapse'; - var EVENT_KEY$3 = "." + DATA_KEY$3; - var DATA_API_KEY$3 = '.data-api'; - var JQUERY_NO_CONFLICT$3 = $__default['default'].fn[NAME$3]; - var Default$1 = { - toggle: true, - parent: '' - }; - var DefaultType$1 = { - toggle: 'boolean', - parent: '(string|element)' - }; - var EVENT_SHOW = "show" + EVENT_KEY$3; - var EVENT_SHOWN = "shown" + EVENT_KEY$3; - var EVENT_HIDE = "hide" + EVENT_KEY$3; - var EVENT_HIDDEN = "hidden" + EVENT_KEY$3; - var EVENT_CLICK_DATA_API$3 = "click" + EVENT_KEY$3 + DATA_API_KEY$3; - var CLASS_NAME_SHOW$1 = 'show'; - var CLASS_NAME_COLLAPSE = 'collapse'; - var CLASS_NAME_COLLAPSING = 'collapsing'; - var CLASS_NAME_COLLAPSED = 'collapsed'; - var DIMENSION_WIDTH = 'width'; - var DIMENSION_HEIGHT = 'height'; - var SELECTOR_ACTIVES = '.show, .collapsing'; - var SELECTOR_DATA_TOGGLE$1 = '[data-toggle="collapse"]'; - /** - * ------------------------------------------------------------------------ - * Class Definition - * ------------------------------------------------------------------------ - */ - - var Collapse = /*#__PURE__*/function () { - function Collapse(element, config) { - this._isTransitioning = false; - this._element = element; - this._config = this._getConfig(config); - this._triggerArray = [].slice.call(document.querySelectorAll("[data-toggle=\"collapse\"][href=\"#" + element.id + "\"]," + ("[data-toggle=\"collapse\"][data-target=\"#" + element.id + "\"]"))); - var toggleList = [].slice.call(document.querySelectorAll(SELECTOR_DATA_TOGGLE$1)); - - for (var i = 0, len = toggleList.length; i < len; i++) { - var elem = toggleList[i]; - var selector = Util.getSelectorFromElement(elem); - var filterElement = [].slice.call(document.querySelectorAll(selector)).filter(function (foundElem) { - return foundElem === element; - }); - - if (selector !== null && filterElement.length > 0) { - this._selector = selector; - - this._triggerArray.push(elem); - } - } - - this._parent = this._config.parent ? this._getParent() : null; - - if (!this._config.parent) { - this._addAriaAndCollapsedClass(this._element, this._triggerArray); - } - - if (this._config.toggle) { - this.toggle(); - } - } // Getters - - - var _proto = Collapse.prototype; - - // Public - _proto.toggle = function toggle() { - if ($__default['default'](this._element).hasClass(CLASS_NAME_SHOW$1)) { - this.hide(); - } else { - this.show(); - } - }; - - _proto.show = function show() { - var _this = this; - - if (this._isTransitioning || $__default['default'](this._element).hasClass(CLASS_NAME_SHOW$1)) { - return; - } - - var actives; - var activesData; - - if (this._parent) { - actives = [].slice.call(this._parent.querySelectorAll(SELECTOR_ACTIVES)).filter(function (elem) { - if (typeof _this._config.parent === 'string') { - return elem.getAttribute('data-parent') === _this._config.parent; - } - - return elem.classList.contains(CLASS_NAME_COLLAPSE); - }); - - if (actives.length === 0) { - actives = null; - } - } - - if (actives) { - activesData = $__default['default'](actives).not(this._selector).data(DATA_KEY$3); - - if (activesData && activesData._isTransitioning) { - return; - } - } - - var startEvent = $__default['default'].Event(EVENT_SHOW); - $__default['default'](this._element).trigger(startEvent); - - if (startEvent.isDefaultPrevented()) { - return; - } - - if (actives) { - Collapse._jQueryInterface.call($__default['default'](actives).not(this._selector), 'hide'); - - if (!activesData) { - $__default['default'](actives).data(DATA_KEY$3, null); - } - } - - var dimension = this._getDimension(); - - $__default['default'](this._element).removeClass(CLASS_NAME_COLLAPSE).addClass(CLASS_NAME_COLLAPSING); - this._element.style[dimension] = 0; - - if (this._triggerArray.length) { - $__default['default'](this._triggerArray).removeClass(CLASS_NAME_COLLAPSED).attr('aria-expanded', true); - } - - this.setTransitioning(true); - - var complete = function complete() { - $__default['default'](_this._element).removeClass(CLASS_NAME_COLLAPSING).addClass(CLASS_NAME_COLLAPSE + " " + CLASS_NAME_SHOW$1); - _this._element.style[dimension] = ''; - - _this.setTransitioning(false); - - $__default['default'](_this._element).trigger(EVENT_SHOWN); - }; - - var capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1); - var scrollSize = "scroll" + capitalizedDimension; - var transitionDuration = Util.getTransitionDurationFromElement(this._element); - $__default['default'](this._element).one(Util.TRANSITION_END, complete).emulateTransitionEnd(transitionDuration); - this._element.style[dimension] = this._element[scrollSize] + "px"; - }; - - _proto.hide = function hide() { - var _this2 = this; - - if (this._isTransitioning || !$__default['default'](this._element).hasClass(CLASS_NAME_SHOW$1)) { - return; - } - - var startEvent = $__default['default'].Event(EVENT_HIDE); - $__default['default'](this._element).trigger(startEvent); - - if (startEvent.isDefaultPrevented()) { - return; - } - - var dimension = this._getDimension(); - - this._element.style[dimension] = this._element.getBoundingClientRect()[dimension] + "px"; - Util.reflow(this._element); - $__default['default'](this._element).addClass(CLASS_NAME_COLLAPSING).removeClass(CLASS_NAME_COLLAPSE + " " + CLASS_NAME_SHOW$1); - var triggerArrayLength = this._triggerArray.length; - - if (triggerArrayLength > 0) { - for (var i = 0; i < triggerArrayLength; i++) { - var trigger = this._triggerArray[i]; - var selector = Util.getSelectorFromElement(trigger); - - if (selector !== null) { - var $elem = $__default['default']([].slice.call(document.querySelectorAll(selector))); - - if (!$elem.hasClass(CLASS_NAME_SHOW$1)) { - $__default['default'](trigger).addClass(CLASS_NAME_COLLAPSED).attr('aria-expanded', false); - } - } - } - } - - this.setTransitioning(true); - - var complete = function complete() { - _this2.setTransitioning(false); - - $__default['default'](_this2._element).removeClass(CLASS_NAME_COLLAPSING).addClass(CLASS_NAME_COLLAPSE).trigger(EVENT_HIDDEN); - }; - - this._element.style[dimension] = ''; - var transitionDuration = Util.getTransitionDurationFromElement(this._element); - $__default['default'](this._element).one(Util.TRANSITION_END, complete).emulateTransitionEnd(transitionDuration); - }; - - _proto.setTransitioning = function setTransitioning(isTransitioning) { - this._isTransitioning = isTransitioning; - }; - - _proto.dispose = function dispose() { - $__default['default'].removeData(this._element, DATA_KEY$3); - this._config = null; - this._parent = null; - this._element = null; - this._triggerArray = null; - this._isTransitioning = null; - } // Private - ; - - _proto._getConfig = function _getConfig(config) { - config = _extends({}, Default$1, config); - config.toggle = Boolean(config.toggle); // Coerce string values - - Util.typeCheckConfig(NAME$3, config, DefaultType$1); - return config; - }; - - _proto._getDimension = function _getDimension() { - var hasWidth = $__default['default'](this._element).hasClass(DIMENSION_WIDTH); - return hasWidth ? DIMENSION_WIDTH : DIMENSION_HEIGHT; - }; - - _proto._getParent = function _getParent() { - var _this3 = this; - - var parent; - - if (Util.isElement(this._config.parent)) { - parent = this._config.parent; // It's a jQuery object - - if (typeof this._config.parent.jquery !== 'undefined') { - parent = this._config.parent[0]; - } - } else { - parent = document.querySelector(this._config.parent); - } - - var selector = "[data-toggle=\"collapse\"][data-parent=\"" + this._config.parent + "\"]"; - var children = [].slice.call(parent.querySelectorAll(selector)); - $__default['default'](children).each(function (i, element) { - _this3._addAriaAndCollapsedClass(Collapse._getTargetFromElement(element), [element]); - }); - return parent; - }; - - _proto._addAriaAndCollapsedClass = function _addAriaAndCollapsedClass(element, triggerArray) { - var isOpen = $__default['default'](element).hasClass(CLASS_NAME_SHOW$1); - - if (triggerArray.length) { - $__default['default'](triggerArray).toggleClass(CLASS_NAME_COLLAPSED, !isOpen).attr('aria-expanded', isOpen); - } - } // Static - ; - - Collapse._getTargetFromElement = function _getTargetFromElement(element) { - var selector = Util.getSelectorFromElement(element); - return selector ? document.querySelector(selector) : null; - }; - - Collapse._jQueryInterface = function _jQueryInterface(config) { - return this.each(function () { - var $element = $__default['default'](this); - var data = $element.data(DATA_KEY$3); - - var _config = _extends({}, Default$1, $element.data(), typeof config === 'object' && config ? config : {}); - - if (!data && _config.toggle && typeof config === 'string' && /show|hide/.test(config)) { - _config.toggle = false; - } - - if (!data) { - data = new Collapse(this, _config); - $element.data(DATA_KEY$3, data); - } - - if (typeof config === 'string') { - if (typeof data[config] === 'undefined') { - throw new TypeError("No method named \"" + config + "\""); - } - - data[config](); - } - }); - }; - - _createClass(Collapse, null, [{ - key: "VERSION", - get: function get() { - return VERSION$3; - } - }, { - key: "Default", - get: function get() { - return Default$1; - } - }]); - - return Collapse; - }(); - /** - * ------------------------------------------------------------------------ - * Data Api implementation - * ------------------------------------------------------------------------ - */ - - - $__default['default'](document).on(EVENT_CLICK_DATA_API$3, SELECTOR_DATA_TOGGLE$1, function (event) { - // preventDefault only for elements (which change the URL) not inside the collapsible element - if (event.currentTarget.tagName === 'A') { - event.preventDefault(); - } - - var $trigger = $__default['default'](this); - var selector = Util.getSelectorFromElement(this); - var selectors = [].slice.call(document.querySelectorAll(selector)); - $__default['default'](selectors).each(function () { - var $target = $__default['default'](this); - var data = $target.data(DATA_KEY$3); - var config = data ? 'toggle' : $trigger.data(); - - Collapse._jQueryInterface.call($target, config); - }); - }); - /** - * ------------------------------------------------------------------------ - * jQuery - * ------------------------------------------------------------------------ - */ - - $__default['default'].fn[NAME$3] = Collapse._jQueryInterface; - $__default['default'].fn[NAME$3].Constructor = Collapse; - - $__default['default'].fn[NAME$3].noConflict = function () { - $__default['default'].fn[NAME$3] = JQUERY_NO_CONFLICT$3; - return Collapse._jQueryInterface; - }; - - /**! - * @fileOverview Kickass library to create and place poppers near their reference elements. - * @version 1.16.1 - * @license - * Copyright (c) 2016 Federico Zivolo and contributors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - var isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined' && typeof navigator !== 'undefined'; - - var timeoutDuration = function () { - var longerTimeoutBrowsers = ['Edge', 'Trident', 'Firefox']; - for (var i = 0; i < longerTimeoutBrowsers.length; i += 1) { - if (isBrowser && navigator.userAgent.indexOf(longerTimeoutBrowsers[i]) >= 0) { - return 1; - } - } - return 0; - }(); - - function microtaskDebounce(fn) { - var called = false; - return function () { - if (called) { - return; - } - called = true; - window.Promise.resolve().then(function () { - called = false; - fn(); - }); - }; - } - - function taskDebounce(fn) { - var scheduled = false; - return function () { - if (!scheduled) { - scheduled = true; - setTimeout(function () { - scheduled = false; - fn(); - }, timeoutDuration); - } - }; - } - - var supportsMicroTasks = isBrowser && window.Promise; - - /** - * Create a debounced version of a method, that's asynchronously deferred - * but called in the minimum time possible. - * - * @method - * @memberof Popper.Utils - * @argument {Function} fn - * @returns {Function} - */ - var debounce = supportsMicroTasks ? microtaskDebounce : taskDebounce; - - /** - * Check if the given variable is a function - * @method - * @memberof Popper.Utils - * @argument {Any} functionToCheck - variable to check - * @returns {Boolean} answer to: is a function? - */ - function isFunction(functionToCheck) { - var getType = {}; - return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]'; - } - - /** - * Get CSS computed property of the given element - * @method - * @memberof Popper.Utils - * @argument {Eement} element - * @argument {String} property - */ - function getStyleComputedProperty(element, property) { - if (element.nodeType !== 1) { - return []; - } - // NOTE: 1 DOM access here - var window = element.ownerDocument.defaultView; - var css = window.getComputedStyle(element, null); - return property ? css[property] : css; - } - - /** - * Returns the parentNode or the host of the element - * @method - * @memberof Popper.Utils - * @argument {Element} element - * @returns {Element} parent - */ - function getParentNode(element) { - if (element.nodeName === 'HTML') { - return element; - } - return element.parentNode || element.host; - } - - /** - * Returns the scrolling parent of the given element - * @method - * @memberof Popper.Utils - * @argument {Element} element - * @returns {Element} scroll parent - */ - function getScrollParent(element) { - // Return body, `getScroll` will take care to get the correct `scrollTop` from it - if (!element) { - return document.body; - } - - switch (element.nodeName) { - case 'HTML': - case 'BODY': - return element.ownerDocument.body; - case '#document': - return element.body; - } - - // Firefox want us to check `-x` and `-y` variations as well - - var _getStyleComputedProp = getStyleComputedProperty(element), - overflow = _getStyleComputedProp.overflow, - overflowX = _getStyleComputedProp.overflowX, - overflowY = _getStyleComputedProp.overflowY; - - if (/(auto|scroll|overlay)/.test(overflow + overflowY + overflowX)) { - return element; - } - - return getScrollParent(getParentNode(element)); - } - - /** - * Returns the reference node of the reference object, or the reference object itself. - * @method - * @memberof Popper.Utils - * @param {Element|Object} reference - the reference element (the popper will be relative to this) - * @returns {Element} parent - */ - function getReferenceNode(reference) { - return reference && reference.referenceNode ? reference.referenceNode : reference; - } - - var isIE11 = isBrowser && !!(window.MSInputMethodContext && document.documentMode); - var isIE10 = isBrowser && /MSIE 10/.test(navigator.userAgent); - - /** - * Determines if the browser is Internet Explorer - * @method - * @memberof Popper.Utils - * @param {Number} version to check - * @returns {Boolean} isIE - */ - function isIE(version) { - if (version === 11) { - return isIE11; - } - if (version === 10) { - return isIE10; - } - return isIE11 || isIE10; - } - - /** - * Returns the offset parent of the given element - * @method - * @memberof Popper.Utils - * @argument {Element} element - * @returns {Element} offset parent - */ - function getOffsetParent(element) { - if (!element) { - return document.documentElement; - } - - var noOffsetParent = isIE(10) ? document.body : null; - - // NOTE: 1 DOM access here - var offsetParent = element.offsetParent || null; - // Skip hidden elements which don't have an offsetParent - while (offsetParent === noOffsetParent && element.nextElementSibling) { - offsetParent = (element = element.nextElementSibling).offsetParent; - } - - var nodeName = offsetParent && offsetParent.nodeName; - - if (!nodeName || nodeName === 'BODY' || nodeName === 'HTML') { - return element ? element.ownerDocument.documentElement : document.documentElement; - } - - // .offsetParent will return the closest TH, TD or TABLE in case - // no offsetParent is present, I hate this job... - if (['TH', 'TD', 'TABLE'].indexOf(offsetParent.nodeName) !== -1 && getStyleComputedProperty(offsetParent, 'position') === 'static') { - return getOffsetParent(offsetParent); - } - - return offsetParent; - } - - function isOffsetContainer(element) { - var nodeName = element.nodeName; - - if (nodeName === 'BODY') { - return false; - } - return nodeName === 'HTML' || getOffsetParent(element.firstElementChild) === element; - } - - /** - * Finds the root node (document, shadowDOM root) of the given element - * @method - * @memberof Popper.Utils - * @argument {Element} node - * @returns {Element} root node - */ - function getRoot(node) { - if (node.parentNode !== null) { - return getRoot(node.parentNode); - } - - return node; - } - - /** - * Finds the offset parent common to the two provided nodes - * @method - * @memberof Popper.Utils - * @argument {Element} element1 - * @argument {Element} element2 - * @returns {Element} common offset parent - */ - function findCommonOffsetParent(element1, element2) { - // This check is needed to avoid errors in case one of the elements isn't defined for any reason - if (!element1 || !element1.nodeType || !element2 || !element2.nodeType) { - return document.documentElement; - } - - // Here we make sure to give as "start" the element that comes first in the DOM - var order = element1.compareDocumentPosition(element2) & Node.DOCUMENT_POSITION_FOLLOWING; - var start = order ? element1 : element2; - var end = order ? element2 : element1; - - // Get common ancestor container - var range = document.createRange(); - range.setStart(start, 0); - range.setEnd(end, 0); - var commonAncestorContainer = range.commonAncestorContainer; - - // Both nodes are inside #document - - if (element1 !== commonAncestorContainer && element2 !== commonAncestorContainer || start.contains(end)) { - if (isOffsetContainer(commonAncestorContainer)) { - return commonAncestorContainer; - } - - return getOffsetParent(commonAncestorContainer); - } - - // one of the nodes is inside shadowDOM, find which one - var element1root = getRoot(element1); - if (element1root.host) { - return findCommonOffsetParent(element1root.host, element2); - } else { - return findCommonOffsetParent(element1, getRoot(element2).host); - } - } - - /** - * Gets the scroll value of the given element in the given side (top and left) - * @method - * @memberof Popper.Utils - * @argument {Element} element - * @argument {String} side `top` or `left` - * @returns {number} amount of scrolled pixels - */ - function getScroll(element) { - var side = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'top'; - - var upperSide = side === 'top' ? 'scrollTop' : 'scrollLeft'; - var nodeName = element.nodeName; - - if (nodeName === 'BODY' || nodeName === 'HTML') { - var html = element.ownerDocument.documentElement; - var scrollingElement = element.ownerDocument.scrollingElement || html; - return scrollingElement[upperSide]; - } - - return element[upperSide]; - } - - /* - * Sum or subtract the element scroll values (left and top) from a given rect object - * @method - * @memberof Popper.Utils - * @param {Object} rect - Rect object you want to change - * @param {HTMLElement} element - The element from the function reads the scroll values - * @param {Boolean} subtract - set to true if you want to subtract the scroll values - * @return {Object} rect - The modifier rect object - */ - function includeScroll(rect, element) { - var subtract = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; - - var scrollTop = getScroll(element, 'top'); - var scrollLeft = getScroll(element, 'left'); - var modifier = subtract ? -1 : 1; - rect.top += scrollTop * modifier; - rect.bottom += scrollTop * modifier; - rect.left += scrollLeft * modifier; - rect.right += scrollLeft * modifier; - return rect; - } - - /* - * Helper to detect borders of a given element - * @method - * @memberof Popper.Utils - * @param {CSSStyleDeclaration} styles - * Result of `getStyleComputedProperty` on the given element - * @param {String} axis - `x` or `y` - * @return {number} borders - The borders size of the given axis - */ - - function getBordersSize(styles, axis) { - var sideA = axis === 'x' ? 'Left' : 'Top'; - var sideB = sideA === 'Left' ? 'Right' : 'Bottom'; - - return parseFloat(styles['border' + sideA + 'Width']) + parseFloat(styles['border' + sideB + 'Width']); - } - - function getSize(axis, body, html, computedStyle) { - return Math.max(body['offset' + axis], body['scroll' + axis], html['client' + axis], html['offset' + axis], html['scroll' + axis], isIE(10) ? parseInt(html['offset' + axis]) + parseInt(computedStyle['margin' + (axis === 'Height' ? 'Top' : 'Left')]) + parseInt(computedStyle['margin' + (axis === 'Height' ? 'Bottom' : 'Right')]) : 0); - } - - function getWindowSizes(document) { - var body = document.body; - var html = document.documentElement; - var computedStyle = isIE(10) && getComputedStyle(html); - - return { - height: getSize('Height', body, html, computedStyle), - width: getSize('Width', body, html, computedStyle) - }; - } - - var classCallCheck = function (instance, Constructor) { - if (!(instance instanceof Constructor)) { - throw new TypeError("Cannot call a class as a function"); - } - }; - - var createClass = function () { - function defineProperties(target, props) { - for (var i = 0; i < props.length; i++) { - var descriptor = props[i]; - descriptor.enumerable = descriptor.enumerable || false; - descriptor.configurable = true; - if ("value" in descriptor) descriptor.writable = true; - Object.defineProperty(target, descriptor.key, descriptor); - } - } - - return function (Constructor, protoProps, staticProps) { - if (protoProps) defineProperties(Constructor.prototype, protoProps); - if (staticProps) defineProperties(Constructor, staticProps); - return Constructor; - }; - }(); - - - - - - var defineProperty = function (obj, key, value) { - if (key in obj) { - Object.defineProperty(obj, key, { - value: value, - enumerable: true, - configurable: true, - writable: true - }); - } else { - obj[key] = value; - } - - return obj; - }; - - var _extends$1 = Object.assign || function (target) { - for (var i = 1; i < arguments.length; i++) { - var source = arguments[i]; - - for (var key in source) { - if (Object.prototype.hasOwnProperty.call(source, key)) { - target[key] = source[key]; - } - } - } - - return target; - }; - - /** - * Given element offsets, generate an output similar to getBoundingClientRect - * @method - * @memberof Popper.Utils - * @argument {Object} offsets - * @returns {Object} ClientRect like output - */ - function getClientRect(offsets) { - return _extends$1({}, offsets, { - right: offsets.left + offsets.width, - bottom: offsets.top + offsets.height - }); - } - - /** - * Get bounding client rect of given element - * @method - * @memberof Popper.Utils - * @param {HTMLElement} element - * @return {Object} client rect - */ - function getBoundingClientRect(element) { - var rect = {}; - - // IE10 10 FIX: Please, don't ask, the element isn't - // considered in DOM in some circumstances... - // This isn't reproducible in IE10 compatibility mode of IE11 - try { - if (isIE(10)) { - rect = element.getBoundingClientRect(); - var scrollTop = getScroll(element, 'top'); - var scrollLeft = getScroll(element, 'left'); - rect.top += scrollTop; - rect.left += scrollLeft; - rect.bottom += scrollTop; - rect.right += scrollLeft; - } else { - rect = element.getBoundingClientRect(); - } - } catch (e) {} - - var result = { - left: rect.left, - top: rect.top, - width: rect.right - rect.left, - height: rect.bottom - rect.top - }; - - // subtract scrollbar size from sizes - var sizes = element.nodeName === 'HTML' ? getWindowSizes(element.ownerDocument) : {}; - var width = sizes.width || element.clientWidth || result.width; - var height = sizes.height || element.clientHeight || result.height; - - var horizScrollbar = element.offsetWidth - width; - var vertScrollbar = element.offsetHeight - height; - - // if an hypothetical scrollbar is detected, we must be sure it's not a `border` - // we make this check conditional for performance reasons - if (horizScrollbar || vertScrollbar) { - var styles = getStyleComputedProperty(element); - horizScrollbar -= getBordersSize(styles, 'x'); - vertScrollbar -= getBordersSize(styles, 'y'); - - result.width -= horizScrollbar; - result.height -= vertScrollbar; - } - - return getClientRect(result); - } - - function getOffsetRectRelativeToArbitraryNode(children, parent) { - var fixedPosition = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; - - var isIE10 = isIE(10); - var isHTML = parent.nodeName === 'HTML'; - var childrenRect = getBoundingClientRect(children); - var parentRect = getBoundingClientRect(parent); - var scrollParent = getScrollParent(children); - - var styles = getStyleComputedProperty(parent); - var borderTopWidth = parseFloat(styles.borderTopWidth); - var borderLeftWidth = parseFloat(styles.borderLeftWidth); - - // In cases where the parent is fixed, we must ignore negative scroll in offset calc - if (fixedPosition && isHTML) { - parentRect.top = Math.max(parentRect.top, 0); - parentRect.left = Math.max(parentRect.left, 0); - } - var offsets = getClientRect({ - top: childrenRect.top - parentRect.top - borderTopWidth, - left: childrenRect.left - parentRect.left - borderLeftWidth, - width: childrenRect.width, - height: childrenRect.height - }); - offsets.marginTop = 0; - offsets.marginLeft = 0; - - // Subtract margins of documentElement in case it's being used as parent - // we do this only on HTML because it's the only element that behaves - // differently when margins are applied to it. The margins are included in - // the box of the documentElement, in the other cases not. - if (!isIE10 && isHTML) { - var marginTop = parseFloat(styles.marginTop); - var marginLeft = parseFloat(styles.marginLeft); - - offsets.top -= borderTopWidth - marginTop; - offsets.bottom -= borderTopWidth - marginTop; - offsets.left -= borderLeftWidth - marginLeft; - offsets.right -= borderLeftWidth - marginLeft; - - // Attach marginTop and marginLeft because in some circumstances we may need them - offsets.marginTop = marginTop; - offsets.marginLeft = marginLeft; - } - - if (isIE10 && !fixedPosition ? parent.contains(scrollParent) : parent === scrollParent && scrollParent.nodeName !== 'BODY') { - offsets = includeScroll(offsets, parent); - } - - return offsets; - } - - function getViewportOffsetRectRelativeToArtbitraryNode(element) { - var excludeScroll = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; - - var html = element.ownerDocument.documentElement; - var relativeOffset = getOffsetRectRelativeToArbitraryNode(element, html); - var width = Math.max(html.clientWidth, window.innerWidth || 0); - var height = Math.max(html.clientHeight, window.innerHeight || 0); - - var scrollTop = !excludeScroll ? getScroll(html) : 0; - var scrollLeft = !excludeScroll ? getScroll(html, 'left') : 0; - - var offset = { - top: scrollTop - relativeOffset.top + relativeOffset.marginTop, - left: scrollLeft - relativeOffset.left + relativeOffset.marginLeft, - width: width, - height: height - }; - - return getClientRect(offset); - } - - /** - * Check if the given element is fixed or is inside a fixed parent - * @method - * @memberof Popper.Utils - * @argument {Element} element - * @argument {Element} customContainer - * @returns {Boolean} answer to "isFixed?" - */ - function isFixed(element) { - var nodeName = element.nodeName; - if (nodeName === 'BODY' || nodeName === 'HTML') { - return false; - } - if (getStyleComputedProperty(element, 'position') === 'fixed') { - return true; - } - var parentNode = getParentNode(element); - if (!parentNode) { - return false; - } - return isFixed(parentNode); - } - - /** - * Finds the first parent of an element that has a transformed property defined - * @method - * @memberof Popper.Utils - * @argument {Element} element - * @returns {Element} first transformed parent or documentElement - */ - - function getFixedPositionOffsetParent(element) { - // This check is needed to avoid errors in case one of the elements isn't defined for any reason - if (!element || !element.parentElement || isIE()) { - return document.documentElement; - } - var el = element.parentElement; - while (el && getStyleComputedProperty(el, 'transform') === 'none') { - el = el.parentElement; - } - return el || document.documentElement; - } - - /** - * Computed the boundaries limits and return them - * @method - * @memberof Popper.Utils - * @param {HTMLElement} popper - * @param {HTMLElement} reference - * @param {number} padding - * @param {HTMLElement} boundariesElement - Element used to define the boundaries - * @param {Boolean} fixedPosition - Is in fixed position mode - * @returns {Object} Coordinates of the boundaries - */ - function getBoundaries(popper, reference, padding, boundariesElement) { - var fixedPosition = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false; - - // NOTE: 1 DOM access here - - var boundaries = { top: 0, left: 0 }; - var offsetParent = fixedPosition ? getFixedPositionOffsetParent(popper) : findCommonOffsetParent(popper, getReferenceNode(reference)); - - // Handle viewport case - if (boundariesElement === 'viewport') { - boundaries = getViewportOffsetRectRelativeToArtbitraryNode(offsetParent, fixedPosition); - } else { - // Handle other cases based on DOM element used as boundaries - var boundariesNode = void 0; - if (boundariesElement === 'scrollParent') { - boundariesNode = getScrollParent(getParentNode(reference)); - if (boundariesNode.nodeName === 'BODY') { - boundariesNode = popper.ownerDocument.documentElement; - } - } else if (boundariesElement === 'window') { - boundariesNode = popper.ownerDocument.documentElement; - } else { - boundariesNode = boundariesElement; - } - - var offsets = getOffsetRectRelativeToArbitraryNode(boundariesNode, offsetParent, fixedPosition); - - // In case of HTML, we need a different computation - if (boundariesNode.nodeName === 'HTML' && !isFixed(offsetParent)) { - var _getWindowSizes = getWindowSizes(popper.ownerDocument), - height = _getWindowSizes.height, - width = _getWindowSizes.width; - - boundaries.top += offsets.top - offsets.marginTop; - boundaries.bottom = height + offsets.top; - boundaries.left += offsets.left - offsets.marginLeft; - boundaries.right = width + offsets.left; - } else { - // for all the other DOM elements, this one is good - boundaries = offsets; - } - } - - // Add paddings - padding = padding || 0; - var isPaddingNumber = typeof padding === 'number'; - boundaries.left += isPaddingNumber ? padding : padding.left || 0; - boundaries.top += isPaddingNumber ? padding : padding.top || 0; - boundaries.right -= isPaddingNumber ? padding : padding.right || 0; - boundaries.bottom -= isPaddingNumber ? padding : padding.bottom || 0; - - return boundaries; - } - - function getArea(_ref) { - var width = _ref.width, - height = _ref.height; - - return width * height; - } - - /** - * Utility used to transform the `auto` placement to the placement with more - * available space. - * @method - * @memberof Popper.Utils - * @argument {Object} data - The data object generated by update method - * @argument {Object} options - Modifiers configuration and options - * @returns {Object} The data object, properly modified - */ - function computeAutoPlacement(placement, refRect, popper, reference, boundariesElement) { - var padding = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : 0; - - if (placement.indexOf('auto') === -1) { - return placement; - } - - var boundaries = getBoundaries(popper, reference, padding, boundariesElement); - - var rects = { - top: { - width: boundaries.width, - height: refRect.top - boundaries.top - }, - right: { - width: boundaries.right - refRect.right, - height: boundaries.height - }, - bottom: { - width: boundaries.width, - height: boundaries.bottom - refRect.bottom - }, - left: { - width: refRect.left - boundaries.left, - height: boundaries.height - } - }; - - var sortedAreas = Object.keys(rects).map(function (key) { - return _extends$1({ - key: key - }, rects[key], { - area: getArea(rects[key]) - }); - }).sort(function (a, b) { - return b.area - a.area; - }); - - var filteredAreas = sortedAreas.filter(function (_ref2) { - var width = _ref2.width, - height = _ref2.height; - return width >= popper.clientWidth && height >= popper.clientHeight; - }); - - var computedPlacement = filteredAreas.length > 0 ? filteredAreas[0].key : sortedAreas[0].key; - - var variation = placement.split('-')[1]; - - return computedPlacement + (variation ? '-' + variation : ''); - } - - /** - * Get offsets to the reference element - * @method - * @memberof Popper.Utils - * @param {Object} state - * @param {Element} popper - the popper element - * @param {Element} reference - the reference element (the popper will be relative to this) - * @param {Element} fixedPosition - is in fixed position mode - * @returns {Object} An object containing the offsets which will be applied to the popper - */ - function getReferenceOffsets(state, popper, reference) { - var fixedPosition = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null; - - var commonOffsetParent = fixedPosition ? getFixedPositionOffsetParent(popper) : findCommonOffsetParent(popper, getReferenceNode(reference)); - return getOffsetRectRelativeToArbitraryNode(reference, commonOffsetParent, fixedPosition); - } - - /** - * Get the outer sizes of the given element (offset size + margins) - * @method - * @memberof Popper.Utils - * @argument {Element} element - * @returns {Object} object containing width and height properties - */ - function getOuterSizes(element) { - var window = element.ownerDocument.defaultView; - var styles = window.getComputedStyle(element); - var x = parseFloat(styles.marginTop || 0) + parseFloat(styles.marginBottom || 0); - var y = parseFloat(styles.marginLeft || 0) + parseFloat(styles.marginRight || 0); - var result = { - width: element.offsetWidth + y, - height: element.offsetHeight + x - }; - return result; - } - - /** - * Get the opposite placement of the given one - * @method - * @memberof Popper.Utils - * @argument {String} placement - * @returns {String} flipped placement - */ - function getOppositePlacement(placement) { - var hash = { left: 'right', right: 'left', bottom: 'top', top: 'bottom' }; - return placement.replace(/left|right|bottom|top/g, function (matched) { - return hash[matched]; - }); - } - - /** - * Get offsets to the popper - * @method - * @memberof Popper.Utils - * @param {Object} position - CSS position the Popper will get applied - * @param {HTMLElement} popper - the popper element - * @param {Object} referenceOffsets - the reference offsets (the popper will be relative to this) - * @param {String} placement - one of the valid placement options - * @returns {Object} popperOffsets - An object containing the offsets which will be applied to the popper - */ - function getPopperOffsets(popper, referenceOffsets, placement) { - placement = placement.split('-')[0]; - - // Get popper node sizes - var popperRect = getOuterSizes(popper); - - // Add position, width and height to our offsets object - var popperOffsets = { - width: popperRect.width, - height: popperRect.height - }; - - // depending by the popper placement we have to compute its offsets slightly differently - var isHoriz = ['right', 'left'].indexOf(placement) !== -1; - var mainSide = isHoriz ? 'top' : 'left'; - var secondarySide = isHoriz ? 'left' : 'top'; - var measurement = isHoriz ? 'height' : 'width'; - var secondaryMeasurement = !isHoriz ? 'height' : 'width'; - - popperOffsets[mainSide] = referenceOffsets[mainSide] + referenceOffsets[measurement] / 2 - popperRect[measurement] / 2; - if (placement === secondarySide) { - popperOffsets[secondarySide] = referenceOffsets[secondarySide] - popperRect[secondaryMeasurement]; - } else { - popperOffsets[secondarySide] = referenceOffsets[getOppositePlacement(secondarySide)]; - } - - return popperOffsets; - } - - /** - * Mimics the `find` method of Array - * @method - * @memberof Popper.Utils - * @argument {Array} arr - * @argument prop - * @argument value - * @returns index or -1 - */ - function find(arr, check) { - // use native find if supported - if (Array.prototype.find) { - return arr.find(check); - } - - // use `filter` to obtain the same behavior of `find` - return arr.filter(check)[0]; - } - - /** - * Return the index of the matching object - * @method - * @memberof Popper.Utils - * @argument {Array} arr - * @argument prop - * @argument value - * @returns index or -1 - */ - function findIndex(arr, prop, value) { - // use native findIndex if supported - if (Array.prototype.findIndex) { - return arr.findIndex(function (cur) { - return cur[prop] === value; - }); - } - - // use `find` + `indexOf` if `findIndex` isn't supported - var match = find(arr, function (obj) { - return obj[prop] === value; - }); - return arr.indexOf(match); - } - - /** - * Loop trough the list of modifiers and run them in order, - * each of them will then edit the data object. - * @method - * @memberof Popper.Utils - * @param {dataObject} data - * @param {Array} modifiers - * @param {String} ends - Optional modifier name used as stopper - * @returns {dataObject} - */ - function runModifiers(modifiers, data, ends) { - var modifiersToRun = ends === undefined ? modifiers : modifiers.slice(0, findIndex(modifiers, 'name', ends)); - - modifiersToRun.forEach(function (modifier) { - if (modifier['function']) { - // eslint-disable-line dot-notation - console.warn('`modifier.function` is deprecated, use `modifier.fn`!'); - } - var fn = modifier['function'] || modifier.fn; // eslint-disable-line dot-notation - if (modifier.enabled && isFunction(fn)) { - // Add properties to offsets to make them a complete clientRect object - // we do this before each modifier to make sure the previous one doesn't - // mess with these values - data.offsets.popper = getClientRect(data.offsets.popper); - data.offsets.reference = getClientRect(data.offsets.reference); - - data = fn(data, modifier); - } - }); - - return data; - } - - /** - * Updates the position of the popper, computing the new offsets and applying - * the new style.
    - * Prefer `scheduleUpdate` over `update` because of performance reasons. - * @method - * @memberof Popper - */ - function update() { - // if popper is destroyed, don't perform any further update - if (this.state.isDestroyed) { - return; - } - - var data = { - instance: this, - styles: {}, - arrowStyles: {}, - attributes: {}, - flipped: false, - offsets: {} - }; - - // compute reference element offsets - data.offsets.reference = getReferenceOffsets(this.state, this.popper, this.reference, this.options.positionFixed); - - // compute auto placement, store placement inside the data object, - // modifiers will be able to edit `placement` if needed - // and refer to originalPlacement to know the original value - data.placement = computeAutoPlacement(this.options.placement, data.offsets.reference, this.popper, this.reference, this.options.modifiers.flip.boundariesElement, this.options.modifiers.flip.padding); - - // store the computed placement inside `originalPlacement` - data.originalPlacement = data.placement; - - data.positionFixed = this.options.positionFixed; - - // compute the popper offsets - data.offsets.popper = getPopperOffsets(this.popper, data.offsets.reference, data.placement); - - data.offsets.popper.position = this.options.positionFixed ? 'fixed' : 'absolute'; - - // run the modifiers - data = runModifiers(this.modifiers, data); - - // the first `update` will call `onCreate` callback - // the other ones will call `onUpdate` callback - if (!this.state.isCreated) { - this.state.isCreated = true; - this.options.onCreate(data); - } else { - this.options.onUpdate(data); - } - } - - /** - * Helper used to know if the given modifier is enabled. - * @method - * @memberof Popper.Utils - * @returns {Boolean} - */ - function isModifierEnabled(modifiers, modifierName) { - return modifiers.some(function (_ref) { - var name = _ref.name, - enabled = _ref.enabled; - return enabled && name === modifierName; - }); - } - - /** - * Get the prefixed supported property name - * @method - * @memberof Popper.Utils - * @argument {String} property (camelCase) - * @returns {String} prefixed property (camelCase or PascalCase, depending on the vendor prefix) - */ - function getSupportedPropertyName(property) { - var prefixes = [false, 'ms', 'Webkit', 'Moz', 'O']; - var upperProp = property.charAt(0).toUpperCase() + property.slice(1); - - for (var i = 0; i < prefixes.length; i++) { - var prefix = prefixes[i]; - var toCheck = prefix ? '' + prefix + upperProp : property; - if (typeof document.body.style[toCheck] !== 'undefined') { - return toCheck; - } - } - return null; - } - - /** - * Destroys the popper. - * @method - * @memberof Popper - */ - function destroy() { - this.state.isDestroyed = true; - - // touch DOM only if `applyStyle` modifier is enabled - if (isModifierEnabled(this.modifiers, 'applyStyle')) { - this.popper.removeAttribute('x-placement'); - this.popper.style.position = ''; - this.popper.style.top = ''; - this.popper.style.left = ''; - this.popper.style.right = ''; - this.popper.style.bottom = ''; - this.popper.style.willChange = ''; - this.popper.style[getSupportedPropertyName('transform')] = ''; - } - - this.disableEventListeners(); - - // remove the popper if user explicitly asked for the deletion on destroy - // do not use `remove` because IE11 doesn't support it - if (this.options.removeOnDestroy) { - this.popper.parentNode.removeChild(this.popper); - } - return this; - } - - /** - * Get the window associated with the element - * @argument {Element} element - * @returns {Window} - */ - function getWindow(element) { - var ownerDocument = element.ownerDocument; - return ownerDocument ? ownerDocument.defaultView : window; - } - - function attachToScrollParents(scrollParent, event, callback, scrollParents) { - var isBody = scrollParent.nodeName === 'BODY'; - var target = isBody ? scrollParent.ownerDocument.defaultView : scrollParent; - target.addEventListener(event, callback, { passive: true }); - - if (!isBody) { - attachToScrollParents(getScrollParent(target.parentNode), event, callback, scrollParents); - } - scrollParents.push(target); - } - - /** - * Setup needed event listeners used to update the popper position - * @method - * @memberof Popper.Utils - * @private - */ - function setupEventListeners(reference, options, state, updateBound) { - // Resize event listener on window - state.updateBound = updateBound; - getWindow(reference).addEventListener('resize', state.updateBound, { passive: true }); - - // Scroll event listener on scroll parents - var scrollElement = getScrollParent(reference); - attachToScrollParents(scrollElement, 'scroll', state.updateBound, state.scrollParents); - state.scrollElement = scrollElement; - state.eventsEnabled = true; - - return state; - } - - /** - * It will add resize/scroll events and start recalculating - * position of the popper element when they are triggered. - * @method - * @memberof Popper - */ - function enableEventListeners() { - if (!this.state.eventsEnabled) { - this.state = setupEventListeners(this.reference, this.options, this.state, this.scheduleUpdate); - } - } - - /** - * Remove event listeners used to update the popper position - * @method - * @memberof Popper.Utils - * @private - */ - function removeEventListeners(reference, state) { - // Remove resize event listener on window - getWindow(reference).removeEventListener('resize', state.updateBound); - - // Remove scroll event listener on scroll parents - state.scrollParents.forEach(function (target) { - target.removeEventListener('scroll', state.updateBound); - }); - - // Reset state - state.updateBound = null; - state.scrollParents = []; - state.scrollElement = null; - state.eventsEnabled = false; - return state; - } - - /** - * It will remove resize/scroll events and won't recalculate popper position - * when they are triggered. It also won't trigger `onUpdate` callback anymore, - * unless you call `update` method manually. - * @method - * @memberof Popper - */ - function disableEventListeners() { - if (this.state.eventsEnabled) { - cancelAnimationFrame(this.scheduleUpdate); - this.state = removeEventListeners(this.reference, this.state); - } - } - - /** - * Tells if a given input is a number - * @method - * @memberof Popper.Utils - * @param {*} input to check - * @return {Boolean} - */ - function isNumeric(n) { - return n !== '' && !isNaN(parseFloat(n)) && isFinite(n); - } - - /** - * Set the style to the given popper - * @method - * @memberof Popper.Utils - * @argument {Element} element - Element to apply the style to - * @argument {Object} styles - * Object with a list of properties and values which will be applied to the element - */ - function setStyles(element, styles) { - Object.keys(styles).forEach(function (prop) { - var unit = ''; - // add unit if the value is numeric and is one of the following - if (['width', 'height', 'top', 'right', 'bottom', 'left'].indexOf(prop) !== -1 && isNumeric(styles[prop])) { - unit = 'px'; - } - element.style[prop] = styles[prop] + unit; - }); - } - - /** - * Set the attributes to the given popper - * @method - * @memberof Popper.Utils - * @argument {Element} element - Element to apply the attributes to - * @argument {Object} styles - * Object with a list of properties and values which will be applied to the element - */ - function setAttributes(element, attributes) { - Object.keys(attributes).forEach(function (prop) { - var value = attributes[prop]; - if (value !== false) { - element.setAttribute(prop, attributes[prop]); - } else { - element.removeAttribute(prop); - } - }); - } - - /** - * @function - * @memberof Modifiers - * @argument {Object} data - The data object generated by `update` method - * @argument {Object} data.styles - List of style properties - values to apply to popper element - * @argument {Object} data.attributes - List of attribute properties - values to apply to popper element - * @argument {Object} options - Modifiers configuration and options - * @returns {Object} The same data object - */ - function applyStyle(data) { - // any property present in `data.styles` will be applied to the popper, - // in this way we can make the 3rd party modifiers add custom styles to it - // Be aware, modifiers could override the properties defined in the previous - // lines of this modifier! - setStyles(data.instance.popper, data.styles); - - // any property present in `data.attributes` will be applied to the popper, - // they will be set as HTML attributes of the element - setAttributes(data.instance.popper, data.attributes); - - // if arrowElement is defined and arrowStyles has some properties - if (data.arrowElement && Object.keys(data.arrowStyles).length) { - setStyles(data.arrowElement, data.arrowStyles); - } - - return data; - } - - /** - * Set the x-placement attribute before everything else because it could be used - * to add margins to the popper margins needs to be calculated to get the - * correct popper offsets. - * @method - * @memberof Popper.modifiers - * @param {HTMLElement} reference - The reference element used to position the popper - * @param {HTMLElement} popper - The HTML element used as popper - * @param {Object} options - Popper.js options - */ - function applyStyleOnLoad(reference, popper, options, modifierOptions, state) { - // compute reference element offsets - var referenceOffsets = getReferenceOffsets(state, popper, reference, options.positionFixed); - - // compute auto placement, store placement inside the data object, - // modifiers will be able to edit `placement` if needed - // and refer to originalPlacement to know the original value - var placement = computeAutoPlacement(options.placement, referenceOffsets, popper, reference, options.modifiers.flip.boundariesElement, options.modifiers.flip.padding); - - popper.setAttribute('x-placement', placement); - - // Apply `position` to popper before anything else because - // without the position applied we can't guarantee correct computations - setStyles(popper, { position: options.positionFixed ? 'fixed' : 'absolute' }); - - return options; - } - - /** - * @function - * @memberof Popper.Utils - * @argument {Object} data - The data object generated by `update` method - * @argument {Boolean} shouldRound - If the offsets should be rounded at all - * @returns {Object} The popper's position offsets rounded - * - * The tale of pixel-perfect positioning. It's still not 100% perfect, but as - * good as it can be within reason. - * Discussion here: https://github.com/FezVrasta/popper.js/pull/715 - * - * Low DPI screens cause a popper to be blurry if not using full pixels (Safari - * as well on High DPI screens). - * - * Firefox prefers no rounding for positioning and does not have blurriness on - * high DPI screens. - * - * Only horizontal placement and left/right values need to be considered. - */ - function getRoundedOffsets(data, shouldRound) { - var _data$offsets = data.offsets, - popper = _data$offsets.popper, - reference = _data$offsets.reference; - var round = Math.round, - floor = Math.floor; - - var noRound = function noRound(v) { - return v; - }; - - var referenceWidth = round(reference.width); - var popperWidth = round(popper.width); - - var isVertical = ['left', 'right'].indexOf(data.placement) !== -1; - var isVariation = data.placement.indexOf('-') !== -1; - var sameWidthParity = referenceWidth % 2 === popperWidth % 2; - var bothOddWidth = referenceWidth % 2 === 1 && popperWidth % 2 === 1; - - var horizontalToInteger = !shouldRound ? noRound : isVertical || isVariation || sameWidthParity ? round : floor; - var verticalToInteger = !shouldRound ? noRound : round; - - return { - left: horizontalToInteger(bothOddWidth && !isVariation && shouldRound ? popper.left - 1 : popper.left), - top: verticalToInteger(popper.top), - bottom: verticalToInteger(popper.bottom), - right: horizontalToInteger(popper.right) - }; - } - - var isFirefox = isBrowser && /Firefox/i.test(navigator.userAgent); - - /** - * @function - * @memberof Modifiers - * @argument {Object} data - The data object generated by `update` method - * @argument {Object} options - Modifiers configuration and options - * @returns {Object} The data object, properly modified - */ - function computeStyle(data, options) { - var x = options.x, - y = options.y; - var popper = data.offsets.popper; - - // Remove this legacy support in Popper.js v2 - - var legacyGpuAccelerationOption = find(data.instance.modifiers, function (modifier) { - return modifier.name === 'applyStyle'; - }).gpuAcceleration; - if (legacyGpuAccelerationOption !== undefined) { - console.warn('WARNING: `gpuAcceleration` option moved to `computeStyle` modifier and will not be supported in future versions of Popper.js!'); - } - var gpuAcceleration = legacyGpuAccelerationOption !== undefined ? legacyGpuAccelerationOption : options.gpuAcceleration; - - var offsetParent = getOffsetParent(data.instance.popper); - var offsetParentRect = getBoundingClientRect(offsetParent); - - // Styles - var styles = { - position: popper.position - }; - - var offsets = getRoundedOffsets(data, window.devicePixelRatio < 2 || !isFirefox); - - var sideA = x === 'bottom' ? 'top' : 'bottom'; - var sideB = y === 'right' ? 'left' : 'right'; - - // if gpuAcceleration is set to `true` and transform is supported, - // we use `translate3d` to apply the position to the popper we - // automatically use the supported prefixed version if needed - var prefixedProperty = getSupportedPropertyName('transform'); - - // now, let's make a step back and look at this code closely (wtf?) - // If the content of the popper grows once it's been positioned, it - // may happen that the popper gets misplaced because of the new content - // overflowing its reference element - // To avoid this problem, we provide two options (x and y), which allow - // the consumer to define the offset origin. - // If we position a popper on top of a reference element, we can set - // `x` to `top` to make the popper grow towards its top instead of - // its bottom. - var left = void 0, - top = void 0; - if (sideA === 'bottom') { - // when offsetParent is the positioning is relative to the bottom of the screen (excluding the scrollbar) - // and not the bottom of the html element - if (offsetParent.nodeName === 'HTML') { - top = -offsetParent.clientHeight + offsets.bottom; - } else { - top = -offsetParentRect.height + offsets.bottom; - } - } else { - top = offsets.top; - } - if (sideB === 'right') { - if (offsetParent.nodeName === 'HTML') { - left = -offsetParent.clientWidth + offsets.right; - } else { - left = -offsetParentRect.width + offsets.right; - } - } else { - left = offsets.left; - } - if (gpuAcceleration && prefixedProperty) { - styles[prefixedProperty] = 'translate3d(' + left + 'px, ' + top + 'px, 0)'; - styles[sideA] = 0; - styles[sideB] = 0; - styles.willChange = 'transform'; - } else { - // othwerise, we use the standard `top`, `left`, `bottom` and `right` properties - var invertTop = sideA === 'bottom' ? -1 : 1; - var invertLeft = sideB === 'right' ? -1 : 1; - styles[sideA] = top * invertTop; - styles[sideB] = left * invertLeft; - styles.willChange = sideA + ', ' + sideB; - } - - // Attributes - var attributes = { - 'x-placement': data.placement - }; - - // Update `data` attributes, styles and arrowStyles - data.attributes = _extends$1({}, attributes, data.attributes); - data.styles = _extends$1({}, styles, data.styles); - data.arrowStyles = _extends$1({}, data.offsets.arrow, data.arrowStyles); - - return data; - } - - /** - * Helper used to know if the given modifier depends from another one.
    - * It checks if the needed modifier is listed and enabled. - * @method - * @memberof Popper.Utils - * @param {Array} modifiers - list of modifiers - * @param {String} requestingName - name of requesting modifier - * @param {String} requestedName - name of requested modifier - * @returns {Boolean} - */ - function isModifierRequired(modifiers, requestingName, requestedName) { - var requesting = find(modifiers, function (_ref) { - var name = _ref.name; - return name === requestingName; - }); - - var isRequired = !!requesting && modifiers.some(function (modifier) { - return modifier.name === requestedName && modifier.enabled && modifier.order < requesting.order; - }); - - if (!isRequired) { - var _requesting = '`' + requestingName + '`'; - var requested = '`' + requestedName + '`'; - console.warn(requested + ' modifier is required by ' + _requesting + ' modifier in order to work, be sure to include it before ' + _requesting + '!'); - } - return isRequired; - } - - /** - * @function - * @memberof Modifiers - * @argument {Object} data - The data object generated by update method - * @argument {Object} options - Modifiers configuration and options - * @returns {Object} The data object, properly modified - */ - function arrow(data, options) { - var _data$offsets$arrow; - - // arrow depends on keepTogether in order to work - if (!isModifierRequired(data.instance.modifiers, 'arrow', 'keepTogether')) { - return data; - } - - var arrowElement = options.element; - - // if arrowElement is a string, suppose it's a CSS selector - if (typeof arrowElement === 'string') { - arrowElement = data.instance.popper.querySelector(arrowElement); - - // if arrowElement is not found, don't run the modifier - if (!arrowElement) { - return data; - } - } else { - // if the arrowElement isn't a query selector we must check that the - // provided DOM node is child of its popper node - if (!data.instance.popper.contains(arrowElement)) { - console.warn('WARNING: `arrow.element` must be child of its popper element!'); - return data; - } - } - - var placement = data.placement.split('-')[0]; - var _data$offsets = data.offsets, - popper = _data$offsets.popper, - reference = _data$offsets.reference; - - var isVertical = ['left', 'right'].indexOf(placement) !== -1; - - var len = isVertical ? 'height' : 'width'; - var sideCapitalized = isVertical ? 'Top' : 'Left'; - var side = sideCapitalized.toLowerCase(); - var altSide = isVertical ? 'left' : 'top'; - var opSide = isVertical ? 'bottom' : 'right'; - var arrowElementSize = getOuterSizes(arrowElement)[len]; - - // - // extends keepTogether behavior making sure the popper and its - // reference have enough pixels in conjunction - // - - // top/left side - if (reference[opSide] - arrowElementSize < popper[side]) { - data.offsets.popper[side] -= popper[side] - (reference[opSide] - arrowElementSize); - } - // bottom/right side - if (reference[side] + arrowElementSize > popper[opSide]) { - data.offsets.popper[side] += reference[side] + arrowElementSize - popper[opSide]; - } - data.offsets.popper = getClientRect(data.offsets.popper); - - // compute center of the popper - var center = reference[side] + reference[len] / 2 - arrowElementSize / 2; - - // Compute the sideValue using the updated popper offsets - // take popper margin in account because we don't have this info available - var css = getStyleComputedProperty(data.instance.popper); - var popperMarginSide = parseFloat(css['margin' + sideCapitalized]); - var popperBorderSide = parseFloat(css['border' + sideCapitalized + 'Width']); - var sideValue = center - data.offsets.popper[side] - popperMarginSide - popperBorderSide; - - // prevent arrowElement from being placed not contiguously to its popper - sideValue = Math.max(Math.min(popper[len] - arrowElementSize, sideValue), 0); - - data.arrowElement = arrowElement; - data.offsets.arrow = (_data$offsets$arrow = {}, defineProperty(_data$offsets$arrow, side, Math.round(sideValue)), defineProperty(_data$offsets$arrow, altSide, ''), _data$offsets$arrow); - - return data; - } - - /** - * Get the opposite placement variation of the given one - * @method - * @memberof Popper.Utils - * @argument {String} placement variation - * @returns {String} flipped placement variation - */ - function getOppositeVariation(variation) { - if (variation === 'end') { - return 'start'; - } else if (variation === 'start') { - return 'end'; - } - return variation; - } - - /** - * List of accepted placements to use as values of the `placement` option.
    - * Valid placements are: - * - `auto` - * - `top` - * - `right` - * - `bottom` - * - `left` - * - * Each placement can have a variation from this list: - * - `-start` - * - `-end` - * - * Variations are interpreted easily if you think of them as the left to right - * written languages. Horizontally (`top` and `bottom`), `start` is left and `end` - * is right.
    - * Vertically (`left` and `right`), `start` is top and `end` is bottom. - * - * Some valid examples are: - * - `top-end` (on top of reference, right aligned) - * - `right-start` (on right of reference, top aligned) - * - `bottom` (on bottom, centered) - * - `auto-end` (on the side with more space available, alignment depends by placement) - * - * @static - * @type {Array} - * @enum {String} - * @readonly - * @method placements - * @memberof Popper - */ - var placements = ['auto-start', 'auto', 'auto-end', 'top-start', 'top', 'top-end', 'right-start', 'right', 'right-end', 'bottom-end', 'bottom', 'bottom-start', 'left-end', 'left', 'left-start']; - - // Get rid of `auto` `auto-start` and `auto-end` - var validPlacements = placements.slice(3); - - /** - * Given an initial placement, returns all the subsequent placements - * clockwise (or counter-clockwise). - * - * @method - * @memberof Popper.Utils - * @argument {String} placement - A valid placement (it accepts variations) - * @argument {Boolean} counter - Set to true to walk the placements counterclockwise - * @returns {Array} placements including their variations - */ - function clockwise(placement) { - var counter = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; - - var index = validPlacements.indexOf(placement); - var arr = validPlacements.slice(index + 1).concat(validPlacements.slice(0, index)); - return counter ? arr.reverse() : arr; - } - - var BEHAVIORS = { - FLIP: 'flip', - CLOCKWISE: 'clockwise', - COUNTERCLOCKWISE: 'counterclockwise' - }; - - /** - * @function - * @memberof Modifiers - * @argument {Object} data - The data object generated by update method - * @argument {Object} options - Modifiers configuration and options - * @returns {Object} The data object, properly modified - */ - function flip(data, options) { - // if `inner` modifier is enabled, we can't use the `flip` modifier - if (isModifierEnabled(data.instance.modifiers, 'inner')) { - return data; - } - - if (data.flipped && data.placement === data.originalPlacement) { - // seems like flip is trying to loop, probably there's not enough space on any of the flippable sides - return data; - } - - var boundaries = getBoundaries(data.instance.popper, data.instance.reference, options.padding, options.boundariesElement, data.positionFixed); - - var placement = data.placement.split('-')[0]; - var placementOpposite = getOppositePlacement(placement); - var variation = data.placement.split('-')[1] || ''; - - var flipOrder = []; - - switch (options.behavior) { - case BEHAVIORS.FLIP: - flipOrder = [placement, placementOpposite]; - break; - case BEHAVIORS.CLOCKWISE: - flipOrder = clockwise(placement); - break; - case BEHAVIORS.COUNTERCLOCKWISE: - flipOrder = clockwise(placement, true); - break; - default: - flipOrder = options.behavior; - } - - flipOrder.forEach(function (step, index) { - if (placement !== step || flipOrder.length === index + 1) { - return data; - } - - placement = data.placement.split('-')[0]; - placementOpposite = getOppositePlacement(placement); - - var popperOffsets = data.offsets.popper; - var refOffsets = data.offsets.reference; - - // using floor because the reference offsets may contain decimals we are not going to consider here - var floor = Math.floor; - var overlapsRef = placement === 'left' && floor(popperOffsets.right) > floor(refOffsets.left) || placement === 'right' && floor(popperOffsets.left) < floor(refOffsets.right) || placement === 'top' && floor(popperOffsets.bottom) > floor(refOffsets.top) || placement === 'bottom' && floor(popperOffsets.top) < floor(refOffsets.bottom); - - var overflowsLeft = floor(popperOffsets.left) < floor(boundaries.left); - var overflowsRight = floor(popperOffsets.right) > floor(boundaries.right); - var overflowsTop = floor(popperOffsets.top) < floor(boundaries.top); - var overflowsBottom = floor(popperOffsets.bottom) > floor(boundaries.bottom); - - var overflowsBoundaries = placement === 'left' && overflowsLeft || placement === 'right' && overflowsRight || placement === 'top' && overflowsTop || placement === 'bottom' && overflowsBottom; - - // flip the variation if required - var isVertical = ['top', 'bottom'].indexOf(placement) !== -1; - - // flips variation if reference element overflows boundaries - var flippedVariationByRef = !!options.flipVariations && (isVertical && variation === 'start' && overflowsLeft || isVertical && variation === 'end' && overflowsRight || !isVertical && variation === 'start' && overflowsTop || !isVertical && variation === 'end' && overflowsBottom); - - // flips variation if popper content overflows boundaries - var flippedVariationByContent = !!options.flipVariationsByContent && (isVertical && variation === 'start' && overflowsRight || isVertical && variation === 'end' && overflowsLeft || !isVertical && variation === 'start' && overflowsBottom || !isVertical && variation === 'end' && overflowsTop); - - var flippedVariation = flippedVariationByRef || flippedVariationByContent; - - if (overlapsRef || overflowsBoundaries || flippedVariation) { - // this boolean to detect any flip loop - data.flipped = true; - - if (overlapsRef || overflowsBoundaries) { - placement = flipOrder[index + 1]; - } - - if (flippedVariation) { - variation = getOppositeVariation(variation); - } - - data.placement = placement + (variation ? '-' + variation : ''); - - // this object contains `position`, we want to preserve it along with - // any additional property we may add in the future - data.offsets.popper = _extends$1({}, data.offsets.popper, getPopperOffsets(data.instance.popper, data.offsets.reference, data.placement)); - - data = runModifiers(data.instance.modifiers, data, 'flip'); - } - }); - return data; - } - - /** - * @function - * @memberof Modifiers - * @argument {Object} data - The data object generated by update method - * @argument {Object} options - Modifiers configuration and options - * @returns {Object} The data object, properly modified - */ - function keepTogether(data) { - var _data$offsets = data.offsets, - popper = _data$offsets.popper, - reference = _data$offsets.reference; - - var placement = data.placement.split('-')[0]; - var floor = Math.floor; - var isVertical = ['top', 'bottom'].indexOf(placement) !== -1; - var side = isVertical ? 'right' : 'bottom'; - var opSide = isVertical ? 'left' : 'top'; - var measurement = isVertical ? 'width' : 'height'; - - if (popper[side] < floor(reference[opSide])) { - data.offsets.popper[opSide] = floor(reference[opSide]) - popper[measurement]; - } - if (popper[opSide] > floor(reference[side])) { - data.offsets.popper[opSide] = floor(reference[side]); - } - - return data; - } - - /** - * Converts a string containing value + unit into a px value number - * @function - * @memberof {modifiers~offset} - * @private - * @argument {String} str - Value + unit string - * @argument {String} measurement - `height` or `width` - * @argument {Object} popperOffsets - * @argument {Object} referenceOffsets - * @returns {Number|String} - * Value in pixels, or original string if no values were extracted - */ - function toValue(str, measurement, popperOffsets, referenceOffsets) { - // separate value from unit - var split = str.match(/((?:\-|\+)?\d*\.?\d*)(.*)/); - var value = +split[1]; - var unit = split[2]; - - // If it's not a number it's an operator, I guess - if (!value) { - return str; - } - - if (unit.indexOf('%') === 0) { - var element = void 0; - switch (unit) { - case '%p': - element = popperOffsets; - break; - case '%': - case '%r': - default: - element = referenceOffsets; - } - - var rect = getClientRect(element); - return rect[measurement] / 100 * value; - } else if (unit === 'vh' || unit === 'vw') { - // if is a vh or vw, we calculate the size based on the viewport - var size = void 0; - if (unit === 'vh') { - size = Math.max(document.documentElement.clientHeight, window.innerHeight || 0); - } else { - size = Math.max(document.documentElement.clientWidth, window.innerWidth || 0); - } - return size / 100 * value; - } else { - // if is an explicit pixel unit, we get rid of the unit and keep the value - // if is an implicit unit, it's px, and we return just the value - return value; - } - } - - /** - * Parse an `offset` string to extrapolate `x` and `y` numeric offsets. - * @function - * @memberof {modifiers~offset} - * @private - * @argument {String} offset - * @argument {Object} popperOffsets - * @argument {Object} referenceOffsets - * @argument {String} basePlacement - * @returns {Array} a two cells array with x and y offsets in numbers - */ - function parseOffset(offset, popperOffsets, referenceOffsets, basePlacement) { - var offsets = [0, 0]; - - // Use height if placement is left or right and index is 0 otherwise use width - // in this way the first offset will use an axis and the second one - // will use the other one - var useHeight = ['right', 'left'].indexOf(basePlacement) !== -1; - - // Split the offset string to obtain a list of values and operands - // The regex addresses values with the plus or minus sign in front (+10, -20, etc) - var fragments = offset.split(/(\+|\-)/).map(function (frag) { - return frag.trim(); - }); - - // Detect if the offset string contains a pair of values or a single one - // they could be separated by comma or space - var divider = fragments.indexOf(find(fragments, function (frag) { - return frag.search(/,|\s/) !== -1; - })); - - if (fragments[divider] && fragments[divider].indexOf(',') === -1) { - console.warn('Offsets separated by white space(s) are deprecated, use a comma (,) instead.'); - } - - // If divider is found, we divide the list of values and operands to divide - // them by ofset X and Y. - var splitRegex = /\s*,\s*|\s+/; - var ops = divider !== -1 ? [fragments.slice(0, divider).concat([fragments[divider].split(splitRegex)[0]]), [fragments[divider].split(splitRegex)[1]].concat(fragments.slice(divider + 1))] : [fragments]; - - // Convert the values with units to absolute pixels to allow our computations - ops = ops.map(function (op, index) { - // Most of the units rely on the orientation of the popper - var measurement = (index === 1 ? !useHeight : useHeight) ? 'height' : 'width'; - var mergeWithPrevious = false; - return op - // This aggregates any `+` or `-` sign that aren't considered operators - // e.g.: 10 + +5 => [10, +, +5] - .reduce(function (a, b) { - if (a[a.length - 1] === '' && ['+', '-'].indexOf(b) !== -1) { - a[a.length - 1] = b; - mergeWithPrevious = true; - return a; - } else if (mergeWithPrevious) { - a[a.length - 1] += b; - mergeWithPrevious = false; - return a; - } else { - return a.concat(b); - } - }, []) - // Here we convert the string values into number values (in px) - .map(function (str) { - return toValue(str, measurement, popperOffsets, referenceOffsets); - }); - }); - - // Loop trough the offsets arrays and execute the operations - ops.forEach(function (op, index) { - op.forEach(function (frag, index2) { - if (isNumeric(frag)) { - offsets[index] += frag * (op[index2 - 1] === '-' ? -1 : 1); - } - }); - }); - return offsets; - } - - /** - * @function - * @memberof Modifiers - * @argument {Object} data - The data object generated by update method - * @argument {Object} options - Modifiers configuration and options - * @argument {Number|String} options.offset=0 - * The offset value as described in the modifier description - * @returns {Object} The data object, properly modified - */ - function offset(data, _ref) { - var offset = _ref.offset; - var placement = data.placement, - _data$offsets = data.offsets, - popper = _data$offsets.popper, - reference = _data$offsets.reference; - - var basePlacement = placement.split('-')[0]; - - var offsets = void 0; - if (isNumeric(+offset)) { - offsets = [+offset, 0]; - } else { - offsets = parseOffset(offset, popper, reference, basePlacement); - } - - if (basePlacement === 'left') { - popper.top += offsets[0]; - popper.left -= offsets[1]; - } else if (basePlacement === 'right') { - popper.top += offsets[0]; - popper.left += offsets[1]; - } else if (basePlacement === 'top') { - popper.left += offsets[0]; - popper.top -= offsets[1]; - } else if (basePlacement === 'bottom') { - popper.left += offsets[0]; - popper.top += offsets[1]; - } - - data.popper = popper; - return data; - } - - /** - * @function - * @memberof Modifiers - * @argument {Object} data - The data object generated by `update` method - * @argument {Object} options - Modifiers configuration and options - * @returns {Object} The data object, properly modified - */ - function preventOverflow(data, options) { - var boundariesElement = options.boundariesElement || getOffsetParent(data.instance.popper); - - // If offsetParent is the reference element, we really want to - // go one step up and use the next offsetParent as reference to - // avoid to make this modifier completely useless and look like broken - if (data.instance.reference === boundariesElement) { - boundariesElement = getOffsetParent(boundariesElement); - } - - // NOTE: DOM access here - // resets the popper's position so that the document size can be calculated excluding - // the size of the popper element itself - var transformProp = getSupportedPropertyName('transform'); - var popperStyles = data.instance.popper.style; // assignment to help minification - var top = popperStyles.top, - left = popperStyles.left, - transform = popperStyles[transformProp]; - - popperStyles.top = ''; - popperStyles.left = ''; - popperStyles[transformProp] = ''; - - var boundaries = getBoundaries(data.instance.popper, data.instance.reference, options.padding, boundariesElement, data.positionFixed); - - // NOTE: DOM access here - // restores the original style properties after the offsets have been computed - popperStyles.top = top; - popperStyles.left = left; - popperStyles[transformProp] = transform; - - options.boundaries = boundaries; - - var order = options.priority; - var popper = data.offsets.popper; - - var check = { - primary: function primary(placement) { - var value = popper[placement]; - if (popper[placement] < boundaries[placement] && !options.escapeWithReference) { - value = Math.max(popper[placement], boundaries[placement]); - } - return defineProperty({}, placement, value); - }, - secondary: function secondary(placement) { - var mainSide = placement === 'right' ? 'left' : 'top'; - var value = popper[mainSide]; - if (popper[placement] > boundaries[placement] && !options.escapeWithReference) { - value = Math.min(popper[mainSide], boundaries[placement] - (placement === 'right' ? popper.width : popper.height)); - } - return defineProperty({}, mainSide, value); - } - }; - - order.forEach(function (placement) { - var side = ['left', 'top'].indexOf(placement) !== -1 ? 'primary' : 'secondary'; - popper = _extends$1({}, popper, check[side](placement)); - }); - - data.offsets.popper = popper; - - return data; - } - - /** - * @function - * @memberof Modifiers - * @argument {Object} data - The data object generated by `update` method - * @argument {Object} options - Modifiers configuration and options - * @returns {Object} The data object, properly modified - */ - function shift(data) { - var placement = data.placement; - var basePlacement = placement.split('-')[0]; - var shiftvariation = placement.split('-')[1]; - - // if shift shiftvariation is specified, run the modifier - if (shiftvariation) { - var _data$offsets = data.offsets, - reference = _data$offsets.reference, - popper = _data$offsets.popper; - - var isVertical = ['bottom', 'top'].indexOf(basePlacement) !== -1; - var side = isVertical ? 'left' : 'top'; - var measurement = isVertical ? 'width' : 'height'; - - var shiftOffsets = { - start: defineProperty({}, side, reference[side]), - end: defineProperty({}, side, reference[side] + reference[measurement] - popper[measurement]) - }; - - data.offsets.popper = _extends$1({}, popper, shiftOffsets[shiftvariation]); - } - - return data; - } - - /** - * @function - * @memberof Modifiers - * @argument {Object} data - The data object generated by update method - * @argument {Object} options - Modifiers configuration and options - * @returns {Object} The data object, properly modified - */ - function hide(data) { - if (!isModifierRequired(data.instance.modifiers, 'hide', 'preventOverflow')) { - return data; - } - - var refRect = data.offsets.reference; - var bound = find(data.instance.modifiers, function (modifier) { - return modifier.name === 'preventOverflow'; - }).boundaries; - - if (refRect.bottom < bound.top || refRect.left > bound.right || refRect.top > bound.bottom || refRect.right < bound.left) { - // Avoid unnecessary DOM access if visibility hasn't changed - if (data.hide === true) { - return data; - } - - data.hide = true; - data.attributes['x-out-of-boundaries'] = ''; - } else { - // Avoid unnecessary DOM access if visibility hasn't changed - if (data.hide === false) { - return data; - } - - data.hide = false; - data.attributes['x-out-of-boundaries'] = false; - } - - return data; - } - - /** - * @function - * @memberof Modifiers - * @argument {Object} data - The data object generated by `update` method - * @argument {Object} options - Modifiers configuration and options - * @returns {Object} The data object, properly modified - */ - function inner(data) { - var placement = data.placement; - var basePlacement = placement.split('-')[0]; - var _data$offsets = data.offsets, - popper = _data$offsets.popper, - reference = _data$offsets.reference; - - var isHoriz = ['left', 'right'].indexOf(basePlacement) !== -1; - - var subtractLength = ['top', 'left'].indexOf(basePlacement) === -1; - - popper[isHoriz ? 'left' : 'top'] = reference[basePlacement] - (subtractLength ? popper[isHoriz ? 'width' : 'height'] : 0); - - data.placement = getOppositePlacement(placement); - data.offsets.popper = getClientRect(popper); - - return data; - } - - /** - * Modifier function, each modifier can have a function of this type assigned - * to its `fn` property.
    - * These functions will be called on each update, this means that you must - * make sure they are performant enough to avoid performance bottlenecks. - * - * @function ModifierFn - * @argument {dataObject} data - The data object generated by `update` method - * @argument {Object} options - Modifiers configuration and options - * @returns {dataObject} The data object, properly modified - */ - - /** - * Modifiers are plugins used to alter the behavior of your poppers.
    - * Popper.js uses a set of 9 modifiers to provide all the basic functionalities - * needed by the library. - * - * Usually you don't want to override the `order`, `fn` and `onLoad` props. - * All the other properties are configurations that could be tweaked. - * @namespace modifiers - */ - var modifiers = { - /** - * Modifier used to shift the popper on the start or end of its reference - * element.
    - * It will read the variation of the `placement` property.
    - * It can be one either `-end` or `-start`. - * @memberof modifiers - * @inner - */ - shift: { - /** @prop {number} order=100 - Index used to define the order of execution */ - order: 100, - /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */ - enabled: true, - /** @prop {ModifierFn} */ - fn: shift - }, - - /** - * The `offset` modifier can shift your popper on both its axis. - * - * It accepts the following units: - * - `px` or unit-less, interpreted as pixels - * - `%` or `%r`, percentage relative to the length of the reference element - * - `%p`, percentage relative to the length of the popper element - * - `vw`, CSS viewport width unit - * - `vh`, CSS viewport height unit - * - * For length is intended the main axis relative to the placement of the popper.
    - * This means that if the placement is `top` or `bottom`, the length will be the - * `width`. In case of `left` or `right`, it will be the `height`. - * - * You can provide a single value (as `Number` or `String`), or a pair of values - * as `String` divided by a comma or one (or more) white spaces.
    - * The latter is a deprecated method because it leads to confusion and will be - * removed in v2.
    - * Additionally, it accepts additions and subtractions between different units. - * Note that multiplications and divisions aren't supported. - * - * Valid examples are: - * ``` - * 10 - * '10%' - * '10, 10' - * '10%, 10' - * '10 + 10%' - * '10 - 5vh + 3%' - * '-10px + 5vh, 5px - 6%' - * ``` - * > **NB**: If you desire to apply offsets to your poppers in a way that may make them overlap - * > with their reference element, unfortunately, you will have to disable the `flip` modifier. - * > You can read more on this at this [issue](https://github.com/FezVrasta/popper.js/issues/373). - * - * @memberof modifiers - * @inner - */ - offset: { - /** @prop {number} order=200 - Index used to define the order of execution */ - order: 200, - /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */ - enabled: true, - /** @prop {ModifierFn} */ - fn: offset, - /** @prop {Number|String} offset=0 - * The offset value as described in the modifier description - */ - offset: 0 - }, - - /** - * Modifier used to prevent the popper from being positioned outside the boundary. - * - * A scenario exists where the reference itself is not within the boundaries.
    - * We can say it has "escaped the boundaries" — or just "escaped".
    - * In this case we need to decide whether the popper should either: - * - * - detach from the reference and remain "trapped" in the boundaries, or - * - if it should ignore the boundary and "escape with its reference" - * - * When `escapeWithReference` is set to`true` and reference is completely - * outside its boundaries, the popper will overflow (or completely leave) - * the boundaries in order to remain attached to the edge of the reference. - * - * @memberof modifiers - * @inner - */ - preventOverflow: { - /** @prop {number} order=300 - Index used to define the order of execution */ - order: 300, - /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */ - enabled: true, - /** @prop {ModifierFn} */ - fn: preventOverflow, - /** - * @prop {Array} [priority=['left','right','top','bottom']] - * Popper will try to prevent overflow following these priorities by default, - * then, it could overflow on the left and on top of the `boundariesElement` - */ - priority: ['left', 'right', 'top', 'bottom'], - /** - * @prop {number} padding=5 - * Amount of pixel used to define a minimum distance between the boundaries - * and the popper. This makes sure the popper always has a little padding - * between the edges of its container - */ - padding: 5, - /** - * @prop {String|HTMLElement} boundariesElement='scrollParent' - * Boundaries used by the modifier. Can be `scrollParent`, `window`, - * `viewport` or any DOM element. - */ - boundariesElement: 'scrollParent' - }, - - /** - * Modifier used to make sure the reference and its popper stay near each other - * without leaving any gap between the two. Especially useful when the arrow is - * enabled and you want to ensure that it points to its reference element. - * It cares only about the first axis. You can still have poppers with margin - * between the popper and its reference element. - * @memberof modifiers - * @inner - */ - keepTogether: { - /** @prop {number} order=400 - Index used to define the order of execution */ - order: 400, - /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */ - enabled: true, - /** @prop {ModifierFn} */ - fn: keepTogether - }, - - /** - * This modifier is used to move the `arrowElement` of the popper to make - * sure it is positioned between the reference element and its popper element. - * It will read the outer size of the `arrowElement` node to detect how many - * pixels of conjunction are needed. - * - * It has no effect if no `arrowElement` is provided. - * @memberof modifiers - * @inner - */ - arrow: { - /** @prop {number} order=500 - Index used to define the order of execution */ - order: 500, - /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */ - enabled: true, - /** @prop {ModifierFn} */ - fn: arrow, - /** @prop {String|HTMLElement} element='[x-arrow]' - Selector or node used as arrow */ - element: '[x-arrow]' - }, - - /** - * Modifier used to flip the popper's placement when it starts to overlap its - * reference element. - * - * Requires the `preventOverflow` modifier before it in order to work. - * - * **NOTE:** this modifier will interrupt the current update cycle and will - * restart it if it detects the need to flip the placement. - * @memberof modifiers - * @inner - */ - flip: { - /** @prop {number} order=600 - Index used to define the order of execution */ - order: 600, - /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */ - enabled: true, - /** @prop {ModifierFn} */ - fn: flip, - /** - * @prop {String|Array} behavior='flip' - * The behavior used to change the popper's placement. It can be one of - * `flip`, `clockwise`, `counterclockwise` or an array with a list of valid - * placements (with optional variations) - */ - behavior: 'flip', - /** - * @prop {number} padding=5 - * The popper will flip if it hits the edges of the `boundariesElement` - */ - padding: 5, - /** - * @prop {String|HTMLElement} boundariesElement='viewport' - * The element which will define the boundaries of the popper position. - * The popper will never be placed outside of the defined boundaries - * (except if `keepTogether` is enabled) - */ - boundariesElement: 'viewport', - /** - * @prop {Boolean} flipVariations=false - * The popper will switch placement variation between `-start` and `-end` when - * the reference element overlaps its boundaries. - * - * The original placement should have a set variation. - */ - flipVariations: false, - /** - * @prop {Boolean} flipVariationsByContent=false - * The popper will switch placement variation between `-start` and `-end` when - * the popper element overlaps its reference boundaries. - * - * The original placement should have a set variation. - */ - flipVariationsByContent: false - }, - - /** - * Modifier used to make the popper flow toward the inner of the reference element. - * By default, when this modifier is disabled, the popper will be placed outside - * the reference element. - * @memberof modifiers - * @inner - */ - inner: { - /** @prop {number} order=700 - Index used to define the order of execution */ - order: 700, - /** @prop {Boolean} enabled=false - Whether the modifier is enabled or not */ - enabled: false, - /** @prop {ModifierFn} */ - fn: inner - }, - - /** - * Modifier used to hide the popper when its reference element is outside of the - * popper boundaries. It will set a `x-out-of-boundaries` attribute which can - * be used to hide with a CSS selector the popper when its reference is - * out of boundaries. - * - * Requires the `preventOverflow` modifier before it in order to work. - * @memberof modifiers - * @inner - */ - hide: { - /** @prop {number} order=800 - Index used to define the order of execution */ - order: 800, - /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */ - enabled: true, - /** @prop {ModifierFn} */ - fn: hide - }, - - /** - * Computes the style that will be applied to the popper element to gets - * properly positioned. - * - * Note that this modifier will not touch the DOM, it just prepares the styles - * so that `applyStyle` modifier can apply it. This separation is useful - * in case you need to replace `applyStyle` with a custom implementation. - * - * This modifier has `850` as `order` value to maintain backward compatibility - * with previous versions of Popper.js. Expect the modifiers ordering method - * to change in future major versions of the library. - * - * @memberof modifiers - * @inner - */ - computeStyle: { - /** @prop {number} order=850 - Index used to define the order of execution */ - order: 850, - /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */ - enabled: true, - /** @prop {ModifierFn} */ - fn: computeStyle, - /** - * @prop {Boolean} gpuAcceleration=true - * If true, it uses the CSS 3D transformation to position the popper. - * Otherwise, it will use the `top` and `left` properties - */ - gpuAcceleration: true, - /** - * @prop {string} [x='bottom'] - * Where to anchor the X axis (`bottom` or `top`). AKA X offset origin. - * Change this if your popper should grow in a direction different from `bottom` - */ - x: 'bottom', - /** - * @prop {string} [x='left'] - * Where to anchor the Y axis (`left` or `right`). AKA Y offset origin. - * Change this if your popper should grow in a direction different from `right` - */ - y: 'right' - }, - - /** - * Applies the computed styles to the popper element. - * - * All the DOM manipulations are limited to this modifier. This is useful in case - * you want to integrate Popper.js inside a framework or view library and you - * want to delegate all the DOM manipulations to it. - * - * Note that if you disable this modifier, you must make sure the popper element - * has its position set to `absolute` before Popper.js can do its work! - * - * Just disable this modifier and define your own to achieve the desired effect. - * - * @memberof modifiers - * @inner - */ - applyStyle: { - /** @prop {number} order=900 - Index used to define the order of execution */ - order: 900, - /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */ - enabled: true, - /** @prop {ModifierFn} */ - fn: applyStyle, - /** @prop {Function} */ - onLoad: applyStyleOnLoad, - /** - * @deprecated since version 1.10.0, the property moved to `computeStyle` modifier - * @prop {Boolean} gpuAcceleration=true - * If true, it uses the CSS 3D transformation to position the popper. - * Otherwise, it will use the `top` and `left` properties - */ - gpuAcceleration: undefined - } - }; - - /** - * The `dataObject` is an object containing all the information used by Popper.js. - * This object is passed to modifiers and to the `onCreate` and `onUpdate` callbacks. - * @name dataObject - * @property {Object} data.instance The Popper.js instance - * @property {String} data.placement Placement applied to popper - * @property {String} data.originalPlacement Placement originally defined on init - * @property {Boolean} data.flipped True if popper has been flipped by flip modifier - * @property {Boolean} data.hide True if the reference element is out of boundaries, useful to know when to hide the popper - * @property {HTMLElement} data.arrowElement Node used as arrow by arrow modifier - * @property {Object} data.styles Any CSS property defined here will be applied to the popper. It expects the JavaScript nomenclature (eg. `marginBottom`) - * @property {Object} data.arrowStyles Any CSS property defined here will be applied to the popper arrow. It expects the JavaScript nomenclature (eg. `marginBottom`) - * @property {Object} data.boundaries Offsets of the popper boundaries - * @property {Object} data.offsets The measurements of popper, reference and arrow elements - * @property {Object} data.offsets.popper `top`, `left`, `width`, `height` values - * @property {Object} data.offsets.reference `top`, `left`, `width`, `height` values - * @property {Object} data.offsets.arrow] `top` and `left` offsets, only one of them will be different from 0 - */ - - /** - * Default options provided to Popper.js constructor.
    - * These can be overridden using the `options` argument of Popper.js.
    - * To override an option, simply pass an object with the same - * structure of the `options` object, as the 3rd argument. For example: - * ``` - * new Popper(ref, pop, { - * modifiers: { - * preventOverflow: { enabled: false } - * } - * }) - * ``` - * @type {Object} - * @static - * @memberof Popper - */ - var Defaults = { - /** - * Popper's placement. - * @prop {Popper.placements} placement='bottom' - */ - placement: 'bottom', - - /** - * Set this to true if you want popper to position it self in 'fixed' mode - * @prop {Boolean} positionFixed=false - */ - positionFixed: false, - - /** - * Whether events (resize, scroll) are initially enabled. - * @prop {Boolean} eventsEnabled=true - */ - eventsEnabled: true, - - /** - * Set to true if you want to automatically remove the popper when - * you call the `destroy` method. - * @prop {Boolean} removeOnDestroy=false - */ - removeOnDestroy: false, - - /** - * Callback called when the popper is created.
    - * By default, it is set to no-op.
    - * Access Popper.js instance with `data.instance`. - * @prop {onCreate} - */ - onCreate: function onCreate() {}, - - /** - * Callback called when the popper is updated. This callback is not called - * on the initialization/creation of the popper, but only on subsequent - * updates.
    - * By default, it is set to no-op.
    - * Access Popper.js instance with `data.instance`. - * @prop {onUpdate} - */ - onUpdate: function onUpdate() {}, - - /** - * List of modifiers used to modify the offsets before they are applied to the popper. - * They provide most of the functionalities of Popper.js. - * @prop {modifiers} - */ - modifiers: modifiers - }; - - /** - * @callback onCreate - * @param {dataObject} data - */ - - /** - * @callback onUpdate - * @param {dataObject} data - */ - - // Utils - // Methods - var Popper = function () { - /** - * Creates a new Popper.js instance. - * @class Popper - * @param {Element|referenceObject} reference - The reference element used to position the popper - * @param {Element} popper - The HTML / XML element used as the popper - * @param {Object} options - Your custom options to override the ones defined in [Defaults](#defaults) - * @return {Object} instance - The generated Popper.js instance - */ - function Popper(reference, popper) { - var _this = this; - - var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; - classCallCheck(this, Popper); - - this.scheduleUpdate = function () { - return requestAnimationFrame(_this.update); - }; - - // make update() debounced, so that it only runs at most once-per-tick - this.update = debounce(this.update.bind(this)); - - // with {} we create a new object with the options inside it - this.options = _extends$1({}, Popper.Defaults, options); - - // init state - this.state = { - isDestroyed: false, - isCreated: false, - scrollParents: [] - }; - - // get reference and popper elements (allow jQuery wrappers) - this.reference = reference && reference.jquery ? reference[0] : reference; - this.popper = popper && popper.jquery ? popper[0] : popper; - - // Deep merge modifiers options - this.options.modifiers = {}; - Object.keys(_extends$1({}, Popper.Defaults.modifiers, options.modifiers)).forEach(function (name) { - _this.options.modifiers[name] = _extends$1({}, Popper.Defaults.modifiers[name] || {}, options.modifiers ? options.modifiers[name] : {}); - }); - - // Refactoring modifiers' list (Object => Array) - this.modifiers = Object.keys(this.options.modifiers).map(function (name) { - return _extends$1({ - name: name - }, _this.options.modifiers[name]); - }) - // sort the modifiers by order - .sort(function (a, b) { - return a.order - b.order; - }); - - // modifiers have the ability to execute arbitrary code when Popper.js get inited - // such code is executed in the same order of its modifier - // they could add new properties to their options configuration - // BE AWARE: don't add options to `options.modifiers.name` but to `modifierOptions`! - this.modifiers.forEach(function (modifierOptions) { - if (modifierOptions.enabled && isFunction(modifierOptions.onLoad)) { - modifierOptions.onLoad(_this.reference, _this.popper, _this.options, modifierOptions, _this.state); - } - }); - - // fire the first update to position the popper in the right place - this.update(); - - var eventsEnabled = this.options.eventsEnabled; - if (eventsEnabled) { - // setup event listeners, they will take care of update the position in specific situations - this.enableEventListeners(); - } - - this.state.eventsEnabled = eventsEnabled; - } - - // We can't use class properties because they don't get listed in the - // class prototype and break stuff like Sinon stubs - - - createClass(Popper, [{ - key: 'update', - value: function update$$1() { - return update.call(this); - } - }, { - key: 'destroy', - value: function destroy$$1() { - return destroy.call(this); - } - }, { - key: 'enableEventListeners', - value: function enableEventListeners$$1() { - return enableEventListeners.call(this); - } - }, { - key: 'disableEventListeners', - value: function disableEventListeners$$1() { - return disableEventListeners.call(this); - } - - /** - * Schedules an update. It will run on the next UI update available. - * @method scheduleUpdate - * @memberof Popper - */ - - - /** - * Collection of utilities useful when writing custom modifiers. - * Starting from version 1.7, this method is available only if you - * include `popper-utils.js` before `popper.js`. - * - * **DEPRECATION**: This way to access PopperUtils is deprecated - * and will be removed in v2! Use the PopperUtils module directly instead. - * Due to the high instability of the methods contained in Utils, we can't - * guarantee them to follow semver. Use them at your own risk! - * @static - * @private - * @type {Object} - * @deprecated since version 1.8 - * @member Utils - * @memberof Popper - */ - - }]); - return Popper; - }(); - - /** - * The `referenceObject` is an object that provides an interface compatible with Popper.js - * and lets you use it as replacement of a real DOM node.
    - * You can use this method to position a popper relatively to a set of coordinates - * in case you don't have a DOM node to use as reference. - * - * ``` - * new Popper(referenceObject, popperNode); - * ``` - * - * NB: This feature isn't supported in Internet Explorer 10. - * @name referenceObject - * @property {Function} data.getBoundingClientRect - * A function that returns a set of coordinates compatible with the native `getBoundingClientRect` method. - * @property {number} data.clientWidth - * An ES6 getter that will return the width of the virtual reference element. - * @property {number} data.clientHeight - * An ES6 getter that will return the height of the virtual reference element. - */ - - - Popper.Utils = (typeof window !== 'undefined' ? window : global).PopperUtils; - Popper.placements = placements; - Popper.Defaults = Defaults; - - /** - * ------------------------------------------------------------------------ - * Constants - * ------------------------------------------------------------------------ - */ - - var NAME$4 = 'dropdown'; - var VERSION$4 = '4.5.3'; - var DATA_KEY$4 = 'bs.dropdown'; - var EVENT_KEY$4 = "." + DATA_KEY$4; - var DATA_API_KEY$4 = '.data-api'; - var JQUERY_NO_CONFLICT$4 = $__default['default'].fn[NAME$4]; - var ESCAPE_KEYCODE = 27; // KeyboardEvent.which value for Escape (Esc) key - - var SPACE_KEYCODE = 32; // KeyboardEvent.which value for space key - - var TAB_KEYCODE = 9; // KeyboardEvent.which value for tab key - - var ARROW_UP_KEYCODE = 38; // KeyboardEvent.which value for up arrow key - - var ARROW_DOWN_KEYCODE = 40; // KeyboardEvent.which value for down arrow key - - var RIGHT_MOUSE_BUTTON_WHICH = 3; // MouseEvent.which value for the right button (assuming a right-handed mouse) - - var REGEXP_KEYDOWN = new RegExp(ARROW_UP_KEYCODE + "|" + ARROW_DOWN_KEYCODE + "|" + ESCAPE_KEYCODE); - var EVENT_HIDE$1 = "hide" + EVENT_KEY$4; - var EVENT_HIDDEN$1 = "hidden" + EVENT_KEY$4; - var EVENT_SHOW$1 = "show" + EVENT_KEY$4; - var EVENT_SHOWN$1 = "shown" + EVENT_KEY$4; - var EVENT_CLICK = "click" + EVENT_KEY$4; - var EVENT_CLICK_DATA_API$4 = "click" + EVENT_KEY$4 + DATA_API_KEY$4; - var EVENT_KEYDOWN_DATA_API = "keydown" + EVENT_KEY$4 + DATA_API_KEY$4; - var EVENT_KEYUP_DATA_API = "keyup" + EVENT_KEY$4 + DATA_API_KEY$4; - var CLASS_NAME_DISABLED = 'disabled'; - var CLASS_NAME_SHOW$2 = 'show'; - var CLASS_NAME_DROPUP = 'dropup'; - var CLASS_NAME_DROPRIGHT = 'dropright'; - var CLASS_NAME_DROPLEFT = 'dropleft'; - var CLASS_NAME_MENURIGHT = 'dropdown-menu-right'; - var CLASS_NAME_POSITION_STATIC = 'position-static'; - var SELECTOR_DATA_TOGGLE$2 = '[data-toggle="dropdown"]'; - var SELECTOR_FORM_CHILD = '.dropdown form'; - var SELECTOR_MENU = '.dropdown-menu'; - var SELECTOR_NAVBAR_NAV = '.navbar-nav'; - var SELECTOR_VISIBLE_ITEMS = '.dropdown-menu .dropdown-item:not(.disabled):not(:disabled)'; - var PLACEMENT_TOP = 'top-start'; - var PLACEMENT_TOPEND = 'top-end'; - var PLACEMENT_BOTTOM = 'bottom-start'; - var PLACEMENT_BOTTOMEND = 'bottom-end'; - var PLACEMENT_RIGHT = 'right-start'; - var PLACEMENT_LEFT = 'left-start'; - var Default$2 = { - offset: 0, - flip: true, - boundary: 'scrollParent', - reference: 'toggle', - display: 'dynamic', - popperConfig: null - }; - var DefaultType$2 = { - offset: '(number|string|function)', - flip: 'boolean', - boundary: '(string|element)', - reference: '(string|element)', - display: 'string', - popperConfig: '(null|object)' - }; - /** - * ------------------------------------------------------------------------ - * Class Definition - * ------------------------------------------------------------------------ - */ - - var Dropdown = /*#__PURE__*/function () { - function Dropdown(element, config) { - this._element = element; - this._popper = null; - this._config = this._getConfig(config); - this._menu = this._getMenuElement(); - this._inNavbar = this._detectNavbar(); - - this._addEventListeners(); - } // Getters - - - var _proto = Dropdown.prototype; - - // Public - _proto.toggle = function toggle() { - if (this._element.disabled || $__default['default'](this._element).hasClass(CLASS_NAME_DISABLED)) { - return; - } - - var isActive = $__default['default'](this._menu).hasClass(CLASS_NAME_SHOW$2); - - Dropdown._clearMenus(); - - if (isActive) { - return; - } - - this.show(true); - }; - - _proto.show = function show(usePopper) { - if (usePopper === void 0) { - usePopper = false; - } - - if (this._element.disabled || $__default['default'](this._element).hasClass(CLASS_NAME_DISABLED) || $__default['default'](this._menu).hasClass(CLASS_NAME_SHOW$2)) { - return; - } - - var relatedTarget = { - relatedTarget: this._element - }; - var showEvent = $__default['default'].Event(EVENT_SHOW$1, relatedTarget); - - var parent = Dropdown._getParentFromElement(this._element); - - $__default['default'](parent).trigger(showEvent); - - if (showEvent.isDefaultPrevented()) { - return; - } // Disable totally Popper.js for Dropdown in Navbar - - - if (!this._inNavbar && usePopper) { - /** - * Check for Popper dependency - * Popper - https://popper.js.org - */ - if (typeof Popper === 'undefined') { - throw new TypeError('Bootstrap\'s dropdowns require Popper.js (https://popper.js.org/)'); - } - - var referenceElement = this._element; - - if (this._config.reference === 'parent') { - referenceElement = parent; - } else if (Util.isElement(this._config.reference)) { - referenceElement = this._config.reference; // Check if it's jQuery element - - if (typeof this._config.reference.jquery !== 'undefined') { - referenceElement = this._config.reference[0]; - } - } // If boundary is not `scrollParent`, then set position to `static` - // to allow the menu to "escape" the scroll parent's boundaries - // https://github.com/twbs/bootstrap/issues/24251 - - - if (this._config.boundary !== 'scrollParent') { - $__default['default'](parent).addClass(CLASS_NAME_POSITION_STATIC); - } - - this._popper = new Popper(referenceElement, this._menu, this._getPopperConfig()); - } // If this is a touch-enabled device we add extra - // empty mouseover listeners to the body's immediate children; - // only needed because of broken event delegation on iOS - // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html - - - if ('ontouchstart' in document.documentElement && $__default['default'](parent).closest(SELECTOR_NAVBAR_NAV).length === 0) { - $__default['default'](document.body).children().on('mouseover', null, $__default['default'].noop); - } - - this._element.focus(); - - this._element.setAttribute('aria-expanded', true); - - $__default['default'](this._menu).toggleClass(CLASS_NAME_SHOW$2); - $__default['default'](parent).toggleClass(CLASS_NAME_SHOW$2).trigger($__default['default'].Event(EVENT_SHOWN$1, relatedTarget)); - }; - - _proto.hide = function hide() { - if (this._element.disabled || $__default['default'](this._element).hasClass(CLASS_NAME_DISABLED) || !$__default['default'](this._menu).hasClass(CLASS_NAME_SHOW$2)) { - return; - } - - var relatedTarget = { - relatedTarget: this._element - }; - var hideEvent = $__default['default'].Event(EVENT_HIDE$1, relatedTarget); - - var parent = Dropdown._getParentFromElement(this._element); - - $__default['default'](parent).trigger(hideEvent); - - if (hideEvent.isDefaultPrevented()) { - return; - } - - if (this._popper) { - this._popper.destroy(); - } - - $__default['default'](this._menu).toggleClass(CLASS_NAME_SHOW$2); - $__default['default'](parent).toggleClass(CLASS_NAME_SHOW$2).trigger($__default['default'].Event(EVENT_HIDDEN$1, relatedTarget)); - }; - - _proto.dispose = function dispose() { - $__default['default'].removeData(this._element, DATA_KEY$4); - $__default['default'](this._element).off(EVENT_KEY$4); - this._element = null; - this._menu = null; - - if (this._popper !== null) { - this._popper.destroy(); - - this._popper = null; - } - }; - - _proto.update = function update() { - this._inNavbar = this._detectNavbar(); - - if (this._popper !== null) { - this._popper.scheduleUpdate(); - } - } // Private - ; - - _proto._addEventListeners = function _addEventListeners() { - var _this = this; - - $__default['default'](this._element).on(EVENT_CLICK, function (event) { - event.preventDefault(); - event.stopPropagation(); - - _this.toggle(); - }); - }; - - _proto._getConfig = function _getConfig(config) { - config = _extends({}, this.constructor.Default, $__default['default'](this._element).data(), config); - Util.typeCheckConfig(NAME$4, config, this.constructor.DefaultType); - return config; - }; - - _proto._getMenuElement = function _getMenuElement() { - if (!this._menu) { - var parent = Dropdown._getParentFromElement(this._element); - - if (parent) { - this._menu = parent.querySelector(SELECTOR_MENU); - } - } - - return this._menu; - }; - - _proto._getPlacement = function _getPlacement() { - var $parentDropdown = $__default['default'](this._element.parentNode); - var placement = PLACEMENT_BOTTOM; // Handle dropup - - if ($parentDropdown.hasClass(CLASS_NAME_DROPUP)) { - placement = $__default['default'](this._menu).hasClass(CLASS_NAME_MENURIGHT) ? PLACEMENT_TOPEND : PLACEMENT_TOP; - } else if ($parentDropdown.hasClass(CLASS_NAME_DROPRIGHT)) { - placement = PLACEMENT_RIGHT; - } else if ($parentDropdown.hasClass(CLASS_NAME_DROPLEFT)) { - placement = PLACEMENT_LEFT; - } else if ($__default['default'](this._menu).hasClass(CLASS_NAME_MENURIGHT)) { - placement = PLACEMENT_BOTTOMEND; - } - - return placement; - }; - - _proto._detectNavbar = function _detectNavbar() { - return $__default['default'](this._element).closest('.navbar').length > 0; - }; - - _proto._getOffset = function _getOffset() { - var _this2 = this; - - var offset = {}; - - if (typeof this._config.offset === 'function') { - offset.fn = function (data) { - data.offsets = _extends({}, data.offsets, _this2._config.offset(data.offsets, _this2._element) || {}); - return data; - }; - } else { - offset.offset = this._config.offset; - } - - return offset; - }; - - _proto._getPopperConfig = function _getPopperConfig() { - var popperConfig = { - placement: this._getPlacement(), - modifiers: { - offset: this._getOffset(), - flip: { - enabled: this._config.flip - }, - preventOverflow: { - boundariesElement: this._config.boundary - } - } - }; // Disable Popper.js if we have a static display - - if (this._config.display === 'static') { - popperConfig.modifiers.applyStyle = { - enabled: false - }; - } - - return _extends({}, popperConfig, this._config.popperConfig); - } // Static - ; - - Dropdown._jQueryInterface = function _jQueryInterface(config) { - return this.each(function () { - var data = $__default['default'](this).data(DATA_KEY$4); - - var _config = typeof config === 'object' ? config : null; - - if (!data) { - data = new Dropdown(this, _config); - $__default['default'](this).data(DATA_KEY$4, data); - } - - if (typeof config === 'string') { - if (typeof data[config] === 'undefined') { - throw new TypeError("No method named \"" + config + "\""); - } - - data[config](); - } - }); - }; - - Dropdown._clearMenus = function _clearMenus(event) { - if (event && (event.which === RIGHT_MOUSE_BUTTON_WHICH || event.type === 'keyup' && event.which !== TAB_KEYCODE)) { - return; - } - - var toggles = [].slice.call(document.querySelectorAll(SELECTOR_DATA_TOGGLE$2)); - - for (var i = 0, len = toggles.length; i < len; i++) { - var parent = Dropdown._getParentFromElement(toggles[i]); - - var context = $__default['default'](toggles[i]).data(DATA_KEY$4); - var relatedTarget = { - relatedTarget: toggles[i] - }; - - if (event && event.type === 'click') { - relatedTarget.clickEvent = event; - } - - if (!context) { - continue; - } - - var dropdownMenu = context._menu; - - if (!$__default['default'](parent).hasClass(CLASS_NAME_SHOW$2)) { - continue; - } - - if (event && (event.type === 'click' && /input|textarea/i.test(event.target.tagName) || event.type === 'keyup' && event.which === TAB_KEYCODE) && $__default['default'].contains(parent, event.target)) { - continue; - } - - var hideEvent = $__default['default'].Event(EVENT_HIDE$1, relatedTarget); - $__default['default'](parent).trigger(hideEvent); - - if (hideEvent.isDefaultPrevented()) { - continue; - } // If this is a touch-enabled device we remove the extra - // empty mouseover listeners we added for iOS support - - - if ('ontouchstart' in document.documentElement) { - $__default['default'](document.body).children().off('mouseover', null, $__default['default'].noop); - } - - toggles[i].setAttribute('aria-expanded', 'false'); - - if (context._popper) { - context._popper.destroy(); - } - - $__default['default'](dropdownMenu).removeClass(CLASS_NAME_SHOW$2); - $__default['default'](parent).removeClass(CLASS_NAME_SHOW$2).trigger($__default['default'].Event(EVENT_HIDDEN$1, relatedTarget)); - } - }; - - Dropdown._getParentFromElement = function _getParentFromElement(element) { - var parent; - var selector = Util.getSelectorFromElement(element); - - if (selector) { - parent = document.querySelector(selector); - } - - return parent || element.parentNode; - } // eslint-disable-next-line complexity - ; - - Dropdown._dataApiKeydownHandler = function _dataApiKeydownHandler(event) { - // If not input/textarea: - // - And not a key in REGEXP_KEYDOWN => not a dropdown command - // If input/textarea: - // - If space key => not a dropdown command - // - If key is other than escape - // - If key is not up or down => not a dropdown command - // - If trigger inside the menu => not a dropdown command - if (/input|textarea/i.test(event.target.tagName) ? event.which === SPACE_KEYCODE || event.which !== ESCAPE_KEYCODE && (event.which !== ARROW_DOWN_KEYCODE && event.which !== ARROW_UP_KEYCODE || $__default['default'](event.target).closest(SELECTOR_MENU).length) : !REGEXP_KEYDOWN.test(event.which)) { - return; - } - - if (this.disabled || $__default['default'](this).hasClass(CLASS_NAME_DISABLED)) { - return; - } - - var parent = Dropdown._getParentFromElement(this); - - var isActive = $__default['default'](parent).hasClass(CLASS_NAME_SHOW$2); - - if (!isActive && event.which === ESCAPE_KEYCODE) { - return; - } - - event.preventDefault(); - event.stopPropagation(); - - if (!isActive || event.which === ESCAPE_KEYCODE || event.which === SPACE_KEYCODE) { - if (event.which === ESCAPE_KEYCODE) { - $__default['default'](parent.querySelector(SELECTOR_DATA_TOGGLE$2)).trigger('focus'); - } - - $__default['default'](this).trigger('click'); - return; - } - - var items = [].slice.call(parent.querySelectorAll(SELECTOR_VISIBLE_ITEMS)).filter(function (item) { - return $__default['default'](item).is(':visible'); - }); - - if (items.length === 0) { - return; - } - - var index = items.indexOf(event.target); - - if (event.which === ARROW_UP_KEYCODE && index > 0) { - // Up - index--; - } - - if (event.which === ARROW_DOWN_KEYCODE && index < items.length - 1) { - // Down - index++; - } - - if (index < 0) { - index = 0; - } - - items[index].focus(); - }; - - _createClass(Dropdown, null, [{ - key: "VERSION", - get: function get() { - return VERSION$4; - } - }, { - key: "Default", - get: function get() { - return Default$2; - } - }, { - key: "DefaultType", - get: function get() { - return DefaultType$2; - } - }]); - - return Dropdown; - }(); - /** - * ------------------------------------------------------------------------ - * Data Api implementation - * ------------------------------------------------------------------------ - */ - - - $__default['default'](document).on(EVENT_KEYDOWN_DATA_API, SELECTOR_DATA_TOGGLE$2, Dropdown._dataApiKeydownHandler).on(EVENT_KEYDOWN_DATA_API, SELECTOR_MENU, Dropdown._dataApiKeydownHandler).on(EVENT_CLICK_DATA_API$4 + " " + EVENT_KEYUP_DATA_API, Dropdown._clearMenus).on(EVENT_CLICK_DATA_API$4, SELECTOR_DATA_TOGGLE$2, function (event) { - event.preventDefault(); - event.stopPropagation(); - - Dropdown._jQueryInterface.call($__default['default'](this), 'toggle'); - }).on(EVENT_CLICK_DATA_API$4, SELECTOR_FORM_CHILD, function (e) { - e.stopPropagation(); - }); - /** - * ------------------------------------------------------------------------ - * jQuery - * ------------------------------------------------------------------------ - */ - - $__default['default'].fn[NAME$4] = Dropdown._jQueryInterface; - $__default['default'].fn[NAME$4].Constructor = Dropdown; - - $__default['default'].fn[NAME$4].noConflict = function () { - $__default['default'].fn[NAME$4] = JQUERY_NO_CONFLICT$4; - return Dropdown._jQueryInterface; - }; - - /** - * ------------------------------------------------------------------------ - * Constants - * ------------------------------------------------------------------------ - */ - - var NAME$5 = 'modal'; - var VERSION$5 = '4.5.3'; - var DATA_KEY$5 = 'bs.modal'; - var EVENT_KEY$5 = "." + DATA_KEY$5; - var DATA_API_KEY$5 = '.data-api'; - var JQUERY_NO_CONFLICT$5 = $__default['default'].fn[NAME$5]; - var ESCAPE_KEYCODE$1 = 27; // KeyboardEvent.which value for Escape (Esc) key - - var Default$3 = { - backdrop: true, - keyboard: true, - focus: true, - show: true - }; - var DefaultType$3 = { - backdrop: '(boolean|string)', - keyboard: 'boolean', - focus: 'boolean', - show: 'boolean' - }; - var EVENT_HIDE$2 = "hide" + EVENT_KEY$5; - var EVENT_HIDE_PREVENTED = "hidePrevented" + EVENT_KEY$5; - var EVENT_HIDDEN$2 = "hidden" + EVENT_KEY$5; - var EVENT_SHOW$2 = "show" + EVENT_KEY$5; - var EVENT_SHOWN$2 = "shown" + EVENT_KEY$5; - var EVENT_FOCUSIN = "focusin" + EVENT_KEY$5; - var EVENT_RESIZE = "resize" + EVENT_KEY$5; - var EVENT_CLICK_DISMISS = "click.dismiss" + EVENT_KEY$5; - var EVENT_KEYDOWN_DISMISS = "keydown.dismiss" + EVENT_KEY$5; - var EVENT_MOUSEUP_DISMISS = "mouseup.dismiss" + EVENT_KEY$5; - var EVENT_MOUSEDOWN_DISMISS = "mousedown.dismiss" + EVENT_KEY$5; - var EVENT_CLICK_DATA_API$5 = "click" + EVENT_KEY$5 + DATA_API_KEY$5; - var CLASS_NAME_SCROLLABLE = 'modal-dialog-scrollable'; - var CLASS_NAME_SCROLLBAR_MEASURER = 'modal-scrollbar-measure'; - var CLASS_NAME_BACKDROP = 'modal-backdrop'; - var CLASS_NAME_OPEN = 'modal-open'; - var CLASS_NAME_FADE$1 = 'fade'; - var CLASS_NAME_SHOW$3 = 'show'; - var CLASS_NAME_STATIC = 'modal-static'; - var SELECTOR_DIALOG = '.modal-dialog'; - var SELECTOR_MODAL_BODY = '.modal-body'; - var SELECTOR_DATA_TOGGLE$3 = '[data-toggle="modal"]'; - var SELECTOR_DATA_DISMISS = '[data-dismiss="modal"]'; - var SELECTOR_FIXED_CONTENT = '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top'; - var SELECTOR_STICKY_CONTENT = '.sticky-top'; - /** - * ------------------------------------------------------------------------ - * Class Definition - * ------------------------------------------------------------------------ - */ - - var Modal = /*#__PURE__*/function () { - function Modal(element, config) { - this._config = this._getConfig(config); - this._element = element; - this._dialog = element.querySelector(SELECTOR_DIALOG); - this._backdrop = null; - this._isShown = false; - this._isBodyOverflowing = false; - this._ignoreBackdropClick = false; - this._isTransitioning = false; - this._scrollbarWidth = 0; - } // Getters - - - var _proto = Modal.prototype; - - // Public - _proto.toggle = function toggle(relatedTarget) { - return this._isShown ? this.hide() : this.show(relatedTarget); - }; - - _proto.show = function show(relatedTarget) { - var _this = this; - - if (this._isShown || this._isTransitioning) { - return; - } - - if ($__default['default'](this._element).hasClass(CLASS_NAME_FADE$1)) { - this._isTransitioning = true; - } - - var showEvent = $__default['default'].Event(EVENT_SHOW$2, { - relatedTarget: relatedTarget - }); - $__default['default'](this._element).trigger(showEvent); - - if (this._isShown || showEvent.isDefaultPrevented()) { - return; - } - - this._isShown = true; - - this._checkScrollbar(); - - this._setScrollbar(); - - this._adjustDialog(); - - this._setEscapeEvent(); - - this._setResizeEvent(); - - $__default['default'](this._element).on(EVENT_CLICK_DISMISS, SELECTOR_DATA_DISMISS, function (event) { - return _this.hide(event); - }); - $__default['default'](this._dialog).on(EVENT_MOUSEDOWN_DISMISS, function () { - $__default['default'](_this._element).one(EVENT_MOUSEUP_DISMISS, function (event) { - if ($__default['default'](event.target).is(_this._element)) { - _this._ignoreBackdropClick = true; - } - }); - }); - - this._showBackdrop(function () { - return _this._showElement(relatedTarget); - }); - }; - - _proto.hide = function hide(event) { - var _this2 = this; - - if (event) { - event.preventDefault(); - } - - if (!this._isShown || this._isTransitioning) { - return; - } - - var hideEvent = $__default['default'].Event(EVENT_HIDE$2); - $__default['default'](this._element).trigger(hideEvent); - - if (!this._isShown || hideEvent.isDefaultPrevented()) { - return; - } - - this._isShown = false; - var transition = $__default['default'](this._element).hasClass(CLASS_NAME_FADE$1); - - if (transition) { - this._isTransitioning = true; - } - - this._setEscapeEvent(); - - this._setResizeEvent(); - - $__default['default'](document).off(EVENT_FOCUSIN); - $__default['default'](this._element).removeClass(CLASS_NAME_SHOW$3); - $__default['default'](this._element).off(EVENT_CLICK_DISMISS); - $__default['default'](this._dialog).off(EVENT_MOUSEDOWN_DISMISS); - - if (transition) { - var transitionDuration = Util.getTransitionDurationFromElement(this._element); - $__default['default'](this._element).one(Util.TRANSITION_END, function (event) { - return _this2._hideModal(event); - }).emulateTransitionEnd(transitionDuration); - } else { - this._hideModal(); - } - }; - - _proto.dispose = function dispose() { - [window, this._element, this._dialog].forEach(function (htmlElement) { - return $__default['default'](htmlElement).off(EVENT_KEY$5); - }); - /** - * `document` has 2 events `EVENT_FOCUSIN` and `EVENT_CLICK_DATA_API` - * Do not move `document` in `htmlElements` array - * It will remove `EVENT_CLICK_DATA_API` event that should remain - */ - - $__default['default'](document).off(EVENT_FOCUSIN); - $__default['default'].removeData(this._element, DATA_KEY$5); - this._config = null; - this._element = null; - this._dialog = null; - this._backdrop = null; - this._isShown = null; - this._isBodyOverflowing = null; - this._ignoreBackdropClick = null; - this._isTransitioning = null; - this._scrollbarWidth = null; - }; - - _proto.handleUpdate = function handleUpdate() { - this._adjustDialog(); - } // Private - ; - - _proto._getConfig = function _getConfig(config) { - config = _extends({}, Default$3, config); - Util.typeCheckConfig(NAME$5, config, DefaultType$3); - return config; - }; - - _proto._triggerBackdropTransition = function _triggerBackdropTransition() { - var _this3 = this; - - if (this._config.backdrop === 'static') { - var hideEventPrevented = $__default['default'].Event(EVENT_HIDE_PREVENTED); - $__default['default'](this._element).trigger(hideEventPrevented); - - if (hideEventPrevented.isDefaultPrevented()) { - return; - } - - var isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight; - - if (!isModalOverflowing) { - this._element.style.overflowY = 'hidden'; - } - - this._element.classList.add(CLASS_NAME_STATIC); - - var modalTransitionDuration = Util.getTransitionDurationFromElement(this._dialog); - $__default['default'](this._element).off(Util.TRANSITION_END); - $__default['default'](this._element).one(Util.TRANSITION_END, function () { - _this3._element.classList.remove(CLASS_NAME_STATIC); - - if (!isModalOverflowing) { - $__default['default'](_this3._element).one(Util.TRANSITION_END, function () { - _this3._element.style.overflowY = ''; - }).emulateTransitionEnd(_this3._element, modalTransitionDuration); - } - }).emulateTransitionEnd(modalTransitionDuration); - - this._element.focus(); - } else { - this.hide(); - } - }; - - _proto._showElement = function _showElement(relatedTarget) { - var _this4 = this; - - var transition = $__default['default'](this._element).hasClass(CLASS_NAME_FADE$1); - var modalBody = this._dialog ? this._dialog.querySelector(SELECTOR_MODAL_BODY) : null; - - if (!this._element.parentNode || this._element.parentNode.nodeType !== Node.ELEMENT_NODE) { - // Don't move modal's DOM position - document.body.appendChild(this._element); - } - - this._element.style.display = 'block'; - - this._element.removeAttribute('aria-hidden'); - - this._element.setAttribute('aria-modal', true); - - this._element.setAttribute('role', 'dialog'); - - if ($__default['default'](this._dialog).hasClass(CLASS_NAME_SCROLLABLE) && modalBody) { - modalBody.scrollTop = 0; - } else { - this._element.scrollTop = 0; - } - - if (transition) { - Util.reflow(this._element); - } - - $__default['default'](this._element).addClass(CLASS_NAME_SHOW$3); - - if (this._config.focus) { - this._enforceFocus(); - } - - var shownEvent = $__default['default'].Event(EVENT_SHOWN$2, { - relatedTarget: relatedTarget - }); - - var transitionComplete = function transitionComplete() { - if (_this4._config.focus) { - _this4._element.focus(); - } - - _this4._isTransitioning = false; - $__default['default'](_this4._element).trigger(shownEvent); - }; - - if (transition) { - var transitionDuration = Util.getTransitionDurationFromElement(this._dialog); - $__default['default'](this._dialog).one(Util.TRANSITION_END, transitionComplete).emulateTransitionEnd(transitionDuration); - } else { - transitionComplete(); - } - }; - - _proto._enforceFocus = function _enforceFocus() { - var _this5 = this; - - $__default['default'](document).off(EVENT_FOCUSIN) // Guard against infinite focus loop - .on(EVENT_FOCUSIN, function (event) { - if (document !== event.target && _this5._element !== event.target && $__default['default'](_this5._element).has(event.target).length === 0) { - _this5._element.focus(); - } - }); - }; - - _proto._setEscapeEvent = function _setEscapeEvent() { - var _this6 = this; - - if (this._isShown) { - $__default['default'](this._element).on(EVENT_KEYDOWN_DISMISS, function (event) { - if (_this6._config.keyboard && event.which === ESCAPE_KEYCODE$1) { - event.preventDefault(); - - _this6.hide(); - } else if (!_this6._config.keyboard && event.which === ESCAPE_KEYCODE$1) { - _this6._triggerBackdropTransition(); - } - }); - } else if (!this._isShown) { - $__default['default'](this._element).off(EVENT_KEYDOWN_DISMISS); - } - }; - - _proto._setResizeEvent = function _setResizeEvent() { - var _this7 = this; - - if (this._isShown) { - $__default['default'](window).on(EVENT_RESIZE, function (event) { - return _this7.handleUpdate(event); - }); - } else { - $__default['default'](window).off(EVENT_RESIZE); - } - }; - - _proto._hideModal = function _hideModal() { - var _this8 = this; - - this._element.style.display = 'none'; - - this._element.setAttribute('aria-hidden', true); - - this._element.removeAttribute('aria-modal'); - - this._element.removeAttribute('role'); - - this._isTransitioning = false; - - this._showBackdrop(function () { - $__default['default'](document.body).removeClass(CLASS_NAME_OPEN); - - _this8._resetAdjustments(); - - _this8._resetScrollbar(); - - $__default['default'](_this8._element).trigger(EVENT_HIDDEN$2); - }); - }; - - _proto._removeBackdrop = function _removeBackdrop() { - if (this._backdrop) { - $__default['default'](this._backdrop).remove(); - this._backdrop = null; - } - }; - - _proto._showBackdrop = function _showBackdrop(callback) { - var _this9 = this; - - var animate = $__default['default'](this._element).hasClass(CLASS_NAME_FADE$1) ? CLASS_NAME_FADE$1 : ''; - - if (this._isShown && this._config.backdrop) { - this._backdrop = document.createElement('div'); - this._backdrop.className = CLASS_NAME_BACKDROP; - - if (animate) { - this._backdrop.classList.add(animate); - } - - $__default['default'](this._backdrop).appendTo(document.body); - $__default['default'](this._element).on(EVENT_CLICK_DISMISS, function (event) { - if (_this9._ignoreBackdropClick) { - _this9._ignoreBackdropClick = false; - return; - } - - if (event.target !== event.currentTarget) { - return; - } - - _this9._triggerBackdropTransition(); - }); - - if (animate) { - Util.reflow(this._backdrop); - } - - $__default['default'](this._backdrop).addClass(CLASS_NAME_SHOW$3); - - if (!callback) { - return; - } - - if (!animate) { - callback(); - return; - } - - var backdropTransitionDuration = Util.getTransitionDurationFromElement(this._backdrop); - $__default['default'](this._backdrop).one(Util.TRANSITION_END, callback).emulateTransitionEnd(backdropTransitionDuration); - } else if (!this._isShown && this._backdrop) { - $__default['default'](this._backdrop).removeClass(CLASS_NAME_SHOW$3); - - var callbackRemove = function callbackRemove() { - _this9._removeBackdrop(); - - if (callback) { - callback(); - } - }; - - if ($__default['default'](this._element).hasClass(CLASS_NAME_FADE$1)) { - var _backdropTransitionDuration = Util.getTransitionDurationFromElement(this._backdrop); - - $__default['default'](this._backdrop).one(Util.TRANSITION_END, callbackRemove).emulateTransitionEnd(_backdropTransitionDuration); - } else { - callbackRemove(); - } - } else if (callback) { - callback(); - } - } // ---------------------------------------------------------------------- - // the following methods are used to handle overflowing modals - // todo (fat): these should probably be refactored out of modal.js - // ---------------------------------------------------------------------- - ; - - _proto._adjustDialog = function _adjustDialog() { - var isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight; - - if (!this._isBodyOverflowing && isModalOverflowing) { - this._element.style.paddingLeft = this._scrollbarWidth + "px"; - } - - if (this._isBodyOverflowing && !isModalOverflowing) { - this._element.style.paddingRight = this._scrollbarWidth + "px"; - } - }; - - _proto._resetAdjustments = function _resetAdjustments() { - this._element.style.paddingLeft = ''; - this._element.style.paddingRight = ''; - }; - - _proto._checkScrollbar = function _checkScrollbar() { - var rect = document.body.getBoundingClientRect(); - this._isBodyOverflowing = Math.round(rect.left + rect.right) < window.innerWidth; - this._scrollbarWidth = this._getScrollbarWidth(); - }; - - _proto._setScrollbar = function _setScrollbar() { - var _this10 = this; - - if (this._isBodyOverflowing) { - // Note: DOMNode.style.paddingRight returns the actual value or '' if not set - // while $(DOMNode).css('padding-right') returns the calculated value or 0 if not set - var fixedContent = [].slice.call(document.querySelectorAll(SELECTOR_FIXED_CONTENT)); - var stickyContent = [].slice.call(document.querySelectorAll(SELECTOR_STICKY_CONTENT)); // Adjust fixed content padding - - $__default['default'](fixedContent).each(function (index, element) { - var actualPadding = element.style.paddingRight; - var calculatedPadding = $__default['default'](element).css('padding-right'); - $__default['default'](element).data('padding-right', actualPadding).css('padding-right', parseFloat(calculatedPadding) + _this10._scrollbarWidth + "px"); - }); // Adjust sticky content margin - - $__default['default'](stickyContent).each(function (index, element) { - var actualMargin = element.style.marginRight; - var calculatedMargin = $__default['default'](element).css('margin-right'); - $__default['default'](element).data('margin-right', actualMargin).css('margin-right', parseFloat(calculatedMargin) - _this10._scrollbarWidth + "px"); - }); // Adjust body padding - - var actualPadding = document.body.style.paddingRight; - var calculatedPadding = $__default['default'](document.body).css('padding-right'); - $__default['default'](document.body).data('padding-right', actualPadding).css('padding-right', parseFloat(calculatedPadding) + this._scrollbarWidth + "px"); - } - - $__default['default'](document.body).addClass(CLASS_NAME_OPEN); - }; - - _proto._resetScrollbar = function _resetScrollbar() { - // Restore fixed content padding - var fixedContent = [].slice.call(document.querySelectorAll(SELECTOR_FIXED_CONTENT)); - $__default['default'](fixedContent).each(function (index, element) { - var padding = $__default['default'](element).data('padding-right'); - $__default['default'](element).removeData('padding-right'); - element.style.paddingRight = padding ? padding : ''; - }); // Restore sticky content - - var elements = [].slice.call(document.querySelectorAll("" + SELECTOR_STICKY_CONTENT)); - $__default['default'](elements).each(function (index, element) { - var margin = $__default['default'](element).data('margin-right'); - - if (typeof margin !== 'undefined') { - $__default['default'](element).css('margin-right', margin).removeData('margin-right'); - } - }); // Restore body padding - - var padding = $__default['default'](document.body).data('padding-right'); - $__default['default'](document.body).removeData('padding-right'); - document.body.style.paddingRight = padding ? padding : ''; - }; - - _proto._getScrollbarWidth = function _getScrollbarWidth() { - // thx d.walsh - var scrollDiv = document.createElement('div'); - scrollDiv.className = CLASS_NAME_SCROLLBAR_MEASURER; - document.body.appendChild(scrollDiv); - var scrollbarWidth = scrollDiv.getBoundingClientRect().width - scrollDiv.clientWidth; - document.body.removeChild(scrollDiv); - return scrollbarWidth; - } // Static - ; - - Modal._jQueryInterface = function _jQueryInterface(config, relatedTarget) { - return this.each(function () { - var data = $__default['default'](this).data(DATA_KEY$5); - - var _config = _extends({}, Default$3, $__default['default'](this).data(), typeof config === 'object' && config ? config : {}); - - if (!data) { - data = new Modal(this, _config); - $__default['default'](this).data(DATA_KEY$5, data); - } - - if (typeof config === 'string') { - if (typeof data[config] === 'undefined') { - throw new TypeError("No method named \"" + config + "\""); - } - - data[config](relatedTarget); - } else if (_config.show) { - data.show(relatedTarget); - } - }); - }; - - _createClass(Modal, null, [{ - key: "VERSION", - get: function get() { - return VERSION$5; - } - }, { - key: "Default", - get: function get() { - return Default$3; - } - }]); - - return Modal; - }(); - /** - * ------------------------------------------------------------------------ - * Data Api implementation - * ------------------------------------------------------------------------ - */ - - - $__default['default'](document).on(EVENT_CLICK_DATA_API$5, SELECTOR_DATA_TOGGLE$3, function (event) { - var _this11 = this; - - var target; - var selector = Util.getSelectorFromElement(this); - - if (selector) { - target = document.querySelector(selector); - } - - var config = $__default['default'](target).data(DATA_KEY$5) ? 'toggle' : _extends({}, $__default['default'](target).data(), $__default['default'](this).data()); - - if (this.tagName === 'A' || this.tagName === 'AREA') { - event.preventDefault(); - } - - var $target = $__default['default'](target).one(EVENT_SHOW$2, function (showEvent) { - if (showEvent.isDefaultPrevented()) { - // Only register focus restorer if modal will actually get shown - return; - } - - $target.one(EVENT_HIDDEN$2, function () { - if ($__default['default'](_this11).is(':visible')) { - _this11.focus(); - } - }); - }); - - Modal._jQueryInterface.call($__default['default'](target), config, this); - }); - /** - * ------------------------------------------------------------------------ - * jQuery - * ------------------------------------------------------------------------ - */ - - $__default['default'].fn[NAME$5] = Modal._jQueryInterface; - $__default['default'].fn[NAME$5].Constructor = Modal; - - $__default['default'].fn[NAME$5].noConflict = function () { - $__default['default'].fn[NAME$5] = JQUERY_NO_CONFLICT$5; - return Modal._jQueryInterface; - }; - - /** - * -------------------------------------------------------------------------- - * Bootstrap (v4.5.3): tools/sanitizer.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - var uriAttrs = ['background', 'cite', 'href', 'itemtype', 'longdesc', 'poster', 'src', 'xlink:href']; - var ARIA_ATTRIBUTE_PATTERN = /^aria-[\w-]*$/i; - var DefaultWhitelist = { - // Global attributes allowed on any supplied element below. - '*': ['class', 'dir', 'id', 'lang', 'role', ARIA_ATTRIBUTE_PATTERN], - a: ['target', 'href', 'title', 'rel'], - area: [], - b: [], - br: [], - col: [], - code: [], - div: [], - em: [], - hr: [], - h1: [], - h2: [], - h3: [], - h4: [], - h5: [], - h6: [], - i: [], - img: ['src', 'srcset', 'alt', 'title', 'width', 'height'], - li: [], - ol: [], - p: [], - pre: [], - s: [], - small: [], - span: [], - sub: [], - sup: [], - strong: [], - u: [], - ul: [] - }; - /** - * A pattern that recognizes a commonly useful subset of URLs that are safe. - * - * Shoutout to Angular 7 https://github.com/angular/angular/blob/7.2.4/packages/core/src/sanitization/url_sanitizer.ts - */ - - var SAFE_URL_PATTERN = /^(?:(?:https?|mailto|ftp|tel|file):|[^#&/:?]*(?:[#/?]|$))/gi; - /** - * A pattern that matches safe data URLs. Only matches image, video and audio types. - * - * Shoutout to Angular 7 https://github.com/angular/angular/blob/7.2.4/packages/core/src/sanitization/url_sanitizer.ts - */ - - var DATA_URL_PATTERN = /^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[\d+/a-z]+=*$/i; - - function allowedAttribute(attr, allowedAttributeList) { - var attrName = attr.nodeName.toLowerCase(); - - if (allowedAttributeList.indexOf(attrName) !== -1) { - if (uriAttrs.indexOf(attrName) !== -1) { - return Boolean(attr.nodeValue.match(SAFE_URL_PATTERN) || attr.nodeValue.match(DATA_URL_PATTERN)); - } - - return true; - } - - var regExp = allowedAttributeList.filter(function (attrRegex) { - return attrRegex instanceof RegExp; - }); // Check if a regular expression validates the attribute. - - for (var i = 0, len = regExp.length; i < len; i++) { - if (attrName.match(regExp[i])) { - return true; - } - } - - return false; - } - - function sanitizeHtml(unsafeHtml, whiteList, sanitizeFn) { - if (unsafeHtml.length === 0) { - return unsafeHtml; - } - - if (sanitizeFn && typeof sanitizeFn === 'function') { - return sanitizeFn(unsafeHtml); - } - - var domParser = new window.DOMParser(); - var createdDocument = domParser.parseFromString(unsafeHtml, 'text/html'); - var whitelistKeys = Object.keys(whiteList); - var elements = [].slice.call(createdDocument.body.querySelectorAll('*')); - - var _loop = function _loop(i, len) { - var el = elements[i]; - var elName = el.nodeName.toLowerCase(); - - if (whitelistKeys.indexOf(el.nodeName.toLowerCase()) === -1) { - el.parentNode.removeChild(el); - return "continue"; - } - - var attributeList = [].slice.call(el.attributes); - var whitelistedAttributes = [].concat(whiteList['*'] || [], whiteList[elName] || []); - attributeList.forEach(function (attr) { - if (!allowedAttribute(attr, whitelistedAttributes)) { - el.removeAttribute(attr.nodeName); - } - }); - }; - - for (var i = 0, len = elements.length; i < len; i++) { - var _ret = _loop(i); - - if (_ret === "continue") continue; - } - - return createdDocument.body.innerHTML; - } - - /** - * ------------------------------------------------------------------------ - * Constants - * ------------------------------------------------------------------------ - */ - - var NAME$6 = 'tooltip'; - var VERSION$6 = '4.5.3'; - var DATA_KEY$6 = 'bs.tooltip'; - var EVENT_KEY$6 = "." + DATA_KEY$6; - var JQUERY_NO_CONFLICT$6 = $__default['default'].fn[NAME$6]; - var CLASS_PREFIX = 'bs-tooltip'; - var BSCLS_PREFIX_REGEX = new RegExp("(^|\\s)" + CLASS_PREFIX + "\\S+", 'g'); - var DISALLOWED_ATTRIBUTES = ['sanitize', 'whiteList', 'sanitizeFn']; - var DefaultType$4 = { - animation: 'boolean', - template: 'string', - title: '(string|element|function)', - trigger: 'string', - delay: '(number|object)', - html: 'boolean', - selector: '(string|boolean)', - placement: '(string|function)', - offset: '(number|string|function)', - container: '(string|element|boolean)', - fallbackPlacement: '(string|array)', - boundary: '(string|element)', - sanitize: 'boolean', - sanitizeFn: '(null|function)', - whiteList: 'object', - popperConfig: '(null|object)' - }; - var AttachmentMap = { - AUTO: 'auto', - TOP: 'top', - RIGHT: 'right', - BOTTOM: 'bottom', - LEFT: 'left' - }; - var Default$4 = { - animation: true, - template: '', - trigger: 'hover focus', - title: '', - delay: 0, - html: false, - selector: false, - placement: 'top', - offset: 0, - container: false, - fallbackPlacement: 'flip', - boundary: 'scrollParent', - sanitize: true, - sanitizeFn: null, - whiteList: DefaultWhitelist, - popperConfig: null - }; - var HOVER_STATE_SHOW = 'show'; - var HOVER_STATE_OUT = 'out'; - var Event = { - HIDE: "hide" + EVENT_KEY$6, - HIDDEN: "hidden" + EVENT_KEY$6, - SHOW: "show" + EVENT_KEY$6, - SHOWN: "shown" + EVENT_KEY$6, - INSERTED: "inserted" + EVENT_KEY$6, - CLICK: "click" + EVENT_KEY$6, - FOCUSIN: "focusin" + EVENT_KEY$6, - FOCUSOUT: "focusout" + EVENT_KEY$6, - MOUSEENTER: "mouseenter" + EVENT_KEY$6, - MOUSELEAVE: "mouseleave" + EVENT_KEY$6 - }; - var CLASS_NAME_FADE$2 = 'fade'; - var CLASS_NAME_SHOW$4 = 'show'; - var SELECTOR_TOOLTIP_INNER = '.tooltip-inner'; - var SELECTOR_ARROW = '.arrow'; - var TRIGGER_HOVER = 'hover'; - var TRIGGER_FOCUS = 'focus'; - var TRIGGER_CLICK = 'click'; - var TRIGGER_MANUAL = 'manual'; - /** - * ------------------------------------------------------------------------ - * Class Definition - * ------------------------------------------------------------------------ - */ - - var Tooltip = /*#__PURE__*/function () { - function Tooltip(element, config) { - if (typeof Popper === 'undefined') { - throw new TypeError('Bootstrap\'s tooltips require Popper.js (https://popper.js.org/)'); - } // private - - - this._isEnabled = true; - this._timeout = 0; - this._hoverState = ''; - this._activeTrigger = {}; - this._popper = null; // Protected - - this.element = element; - this.config = this._getConfig(config); - this.tip = null; - - this._setListeners(); - } // Getters - - - var _proto = Tooltip.prototype; - - // Public - _proto.enable = function enable() { - this._isEnabled = true; - }; - - _proto.disable = function disable() { - this._isEnabled = false; - }; - - _proto.toggleEnabled = function toggleEnabled() { - this._isEnabled = !this._isEnabled; - }; - - _proto.toggle = function toggle(event) { - if (!this._isEnabled) { - return; - } - - if (event) { - var dataKey = this.constructor.DATA_KEY; - var context = $__default['default'](event.currentTarget).data(dataKey); - - if (!context) { - context = new this.constructor(event.currentTarget, this._getDelegateConfig()); - $__default['default'](event.currentTarget).data(dataKey, context); - } - - context._activeTrigger.click = !context._activeTrigger.click; - - if (context._isWithActiveTrigger()) { - context._enter(null, context); - } else { - context._leave(null, context); - } - } else { - if ($__default['default'](this.getTipElement()).hasClass(CLASS_NAME_SHOW$4)) { - this._leave(null, this); - - return; - } - - this._enter(null, this); - } - }; - - _proto.dispose = function dispose() { - clearTimeout(this._timeout); - $__default['default'].removeData(this.element, this.constructor.DATA_KEY); - $__default['default'](this.element).off(this.constructor.EVENT_KEY); - $__default['default'](this.element).closest('.modal').off('hide.bs.modal', this._hideModalHandler); - - if (this.tip) { - $__default['default'](this.tip).remove(); - } - - this._isEnabled = null; - this._timeout = null; - this._hoverState = null; - this._activeTrigger = null; - - if (this._popper) { - this._popper.destroy(); - } - - this._popper = null; - this.element = null; - this.config = null; - this.tip = null; - }; - - _proto.show = function show() { - var _this = this; - - if ($__default['default'](this.element).css('display') === 'none') { - throw new Error('Please use show on visible elements'); - } - - var showEvent = $__default['default'].Event(this.constructor.Event.SHOW); - - if (this.isWithContent() && this._isEnabled) { - $__default['default'](this.element).trigger(showEvent); - var shadowRoot = Util.findShadowRoot(this.element); - var isInTheDom = $__default['default'].contains(shadowRoot !== null ? shadowRoot : this.element.ownerDocument.documentElement, this.element); - - if (showEvent.isDefaultPrevented() || !isInTheDom) { - return; - } - - var tip = this.getTipElement(); - var tipId = Util.getUID(this.constructor.NAME); - tip.setAttribute('id', tipId); - this.element.setAttribute('aria-describedby', tipId); - this.setContent(); - - if (this.config.animation) { - $__default['default'](tip).addClass(CLASS_NAME_FADE$2); - } - - var placement = typeof this.config.placement === 'function' ? this.config.placement.call(this, tip, this.element) : this.config.placement; - - var attachment = this._getAttachment(placement); - - this.addAttachmentClass(attachment); - - var container = this._getContainer(); - - $__default['default'](tip).data(this.constructor.DATA_KEY, this); - - if (!$__default['default'].contains(this.element.ownerDocument.documentElement, this.tip)) { - $__default['default'](tip).appendTo(container); - } - - $__default['default'](this.element).trigger(this.constructor.Event.INSERTED); - this._popper = new Popper(this.element, tip, this._getPopperConfig(attachment)); - $__default['default'](tip).addClass(CLASS_NAME_SHOW$4); // If this is a touch-enabled device we add extra - // empty mouseover listeners to the body's immediate children; - // only needed because of broken event delegation on iOS - // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html - - if ('ontouchstart' in document.documentElement) { - $__default['default'](document.body).children().on('mouseover', null, $__default['default'].noop); - } - - var complete = function complete() { - if (_this.config.animation) { - _this._fixTransition(); - } - - var prevHoverState = _this._hoverState; - _this._hoverState = null; - $__default['default'](_this.element).trigger(_this.constructor.Event.SHOWN); - - if (prevHoverState === HOVER_STATE_OUT) { - _this._leave(null, _this); - } - }; - - if ($__default['default'](this.tip).hasClass(CLASS_NAME_FADE$2)) { - var transitionDuration = Util.getTransitionDurationFromElement(this.tip); - $__default['default'](this.tip).one(Util.TRANSITION_END, complete).emulateTransitionEnd(transitionDuration); - } else { - complete(); - } - } - }; - - _proto.hide = function hide(callback) { - var _this2 = this; - - var tip = this.getTipElement(); - var hideEvent = $__default['default'].Event(this.constructor.Event.HIDE); - - var complete = function complete() { - if (_this2._hoverState !== HOVER_STATE_SHOW && tip.parentNode) { - tip.parentNode.removeChild(tip); - } - - _this2._cleanTipClass(); - - _this2.element.removeAttribute('aria-describedby'); - - $__default['default'](_this2.element).trigger(_this2.constructor.Event.HIDDEN); - - if (_this2._popper !== null) { - _this2._popper.destroy(); - } - - if (callback) { - callback(); - } - }; - - $__default['default'](this.element).trigger(hideEvent); - - if (hideEvent.isDefaultPrevented()) { - return; - } - - $__default['default'](tip).removeClass(CLASS_NAME_SHOW$4); // If this is a touch-enabled device we remove the extra - // empty mouseover listeners we added for iOS support - - if ('ontouchstart' in document.documentElement) { - $__default['default'](document.body).children().off('mouseover', null, $__default['default'].noop); - } - - this._activeTrigger[TRIGGER_CLICK] = false; - this._activeTrigger[TRIGGER_FOCUS] = false; - this._activeTrigger[TRIGGER_HOVER] = false; - - if ($__default['default'](this.tip).hasClass(CLASS_NAME_FADE$2)) { - var transitionDuration = Util.getTransitionDurationFromElement(tip); - $__default['default'](tip).one(Util.TRANSITION_END, complete).emulateTransitionEnd(transitionDuration); - } else { - complete(); - } - - this._hoverState = ''; - }; - - _proto.update = function update() { - if (this._popper !== null) { - this._popper.scheduleUpdate(); - } - } // Protected - ; - - _proto.isWithContent = function isWithContent() { - return Boolean(this.getTitle()); - }; - - _proto.addAttachmentClass = function addAttachmentClass(attachment) { - $__default['default'](this.getTipElement()).addClass(CLASS_PREFIX + "-" + attachment); - }; - - _proto.getTipElement = function getTipElement() { - this.tip = this.tip || $__default['default'](this.config.template)[0]; - return this.tip; - }; - - _proto.setContent = function setContent() { - var tip = this.getTipElement(); - this.setElementContent($__default['default'](tip.querySelectorAll(SELECTOR_TOOLTIP_INNER)), this.getTitle()); - $__default['default'](tip).removeClass(CLASS_NAME_FADE$2 + " " + CLASS_NAME_SHOW$4); - }; - - _proto.setElementContent = function setElementContent($element, content) { - if (typeof content === 'object' && (content.nodeType || content.jquery)) { - // Content is a DOM node or a jQuery - if (this.config.html) { - if (!$__default['default'](content).parent().is($element)) { - $element.empty().append(content); - } - } else { - $element.text($__default['default'](content).text()); - } - - return; - } - - if (this.config.html) { - if (this.config.sanitize) { - content = sanitizeHtml(content, this.config.whiteList, this.config.sanitizeFn); - } - - $element.html(content); - } else { - $element.text(content); - } - }; - - _proto.getTitle = function getTitle() { - var title = this.element.getAttribute('data-original-title'); - - if (!title) { - title = typeof this.config.title === 'function' ? this.config.title.call(this.element) : this.config.title; - } - - return title; - } // Private - ; - - _proto._getPopperConfig = function _getPopperConfig(attachment) { - var _this3 = this; - - var defaultBsConfig = { - placement: attachment, - modifiers: { - offset: this._getOffset(), - flip: { - behavior: this.config.fallbackPlacement - }, - arrow: { - element: SELECTOR_ARROW - }, - preventOverflow: { - boundariesElement: this.config.boundary - } - }, - onCreate: function onCreate(data) { - if (data.originalPlacement !== data.placement) { - _this3._handlePopperPlacementChange(data); - } - }, - onUpdate: function onUpdate(data) { - return _this3._handlePopperPlacementChange(data); - } - }; - return _extends({}, defaultBsConfig, this.config.popperConfig); - }; - - _proto._getOffset = function _getOffset() { - var _this4 = this; - - var offset = {}; - - if (typeof this.config.offset === 'function') { - offset.fn = function (data) { - data.offsets = _extends({}, data.offsets, _this4.config.offset(data.offsets, _this4.element) || {}); - return data; - }; - } else { - offset.offset = this.config.offset; - } - - return offset; - }; - - _proto._getContainer = function _getContainer() { - if (this.config.container === false) { - return document.body; - } - - if (Util.isElement(this.config.container)) { - return $__default['default'](this.config.container); - } - - return $__default['default'](document).find(this.config.container); - }; - - _proto._getAttachment = function _getAttachment(placement) { - return AttachmentMap[placement.toUpperCase()]; - }; - - _proto._setListeners = function _setListeners() { - var _this5 = this; - - var triggers = this.config.trigger.split(' '); - triggers.forEach(function (trigger) { - if (trigger === 'click') { - $__default['default'](_this5.element).on(_this5.constructor.Event.CLICK, _this5.config.selector, function (event) { - return _this5.toggle(event); - }); - } else if (trigger !== TRIGGER_MANUAL) { - var eventIn = trigger === TRIGGER_HOVER ? _this5.constructor.Event.MOUSEENTER : _this5.constructor.Event.FOCUSIN; - var eventOut = trigger === TRIGGER_HOVER ? _this5.constructor.Event.MOUSELEAVE : _this5.constructor.Event.FOCUSOUT; - $__default['default'](_this5.element).on(eventIn, _this5.config.selector, function (event) { - return _this5._enter(event); - }).on(eventOut, _this5.config.selector, function (event) { - return _this5._leave(event); - }); - } - }); - - this._hideModalHandler = function () { - if (_this5.element) { - _this5.hide(); - } - }; - - $__default['default'](this.element).closest('.modal').on('hide.bs.modal', this._hideModalHandler); - - if (this.config.selector) { - this.config = _extends({}, this.config, { - trigger: 'manual', - selector: '' - }); - } else { - this._fixTitle(); - } - }; - - _proto._fixTitle = function _fixTitle() { - var titleType = typeof this.element.getAttribute('data-original-title'); - - if (this.element.getAttribute('title') || titleType !== 'string') { - this.element.setAttribute('data-original-title', this.element.getAttribute('title') || ''); - this.element.setAttribute('title', ''); - } - }; - - _proto._enter = function _enter(event, context) { - var dataKey = this.constructor.DATA_KEY; - context = context || $__default['default'](event.currentTarget).data(dataKey); - - if (!context) { - context = new this.constructor(event.currentTarget, this._getDelegateConfig()); - $__default['default'](event.currentTarget).data(dataKey, context); - } - - if (event) { - context._activeTrigger[event.type === 'focusin' ? TRIGGER_FOCUS : TRIGGER_HOVER] = true; - } - - if ($__default['default'](context.getTipElement()).hasClass(CLASS_NAME_SHOW$4) || context._hoverState === HOVER_STATE_SHOW) { - context._hoverState = HOVER_STATE_SHOW; - return; - } - - clearTimeout(context._timeout); - context._hoverState = HOVER_STATE_SHOW; - - if (!context.config.delay || !context.config.delay.show) { - context.show(); - return; - } - - context._timeout = setTimeout(function () { - if (context._hoverState === HOVER_STATE_SHOW) { - context.show(); - } - }, context.config.delay.show); - }; - - _proto._leave = function _leave(event, context) { - var dataKey = this.constructor.DATA_KEY; - context = context || $__default['default'](event.currentTarget).data(dataKey); - - if (!context) { - context = new this.constructor(event.currentTarget, this._getDelegateConfig()); - $__default['default'](event.currentTarget).data(dataKey, context); - } - - if (event) { - context._activeTrigger[event.type === 'focusout' ? TRIGGER_FOCUS : TRIGGER_HOVER] = false; - } - - if (context._isWithActiveTrigger()) { - return; - } - - clearTimeout(context._timeout); - context._hoverState = HOVER_STATE_OUT; - - if (!context.config.delay || !context.config.delay.hide) { - context.hide(); - return; - } - - context._timeout = setTimeout(function () { - if (context._hoverState === HOVER_STATE_OUT) { - context.hide(); - } - }, context.config.delay.hide); - }; - - _proto._isWithActiveTrigger = function _isWithActiveTrigger() { - for (var trigger in this._activeTrigger) { - if (this._activeTrigger[trigger]) { - return true; - } - } - - return false; - }; - - _proto._getConfig = function _getConfig(config) { - var dataAttributes = $__default['default'](this.element).data(); - Object.keys(dataAttributes).forEach(function (dataAttr) { - if (DISALLOWED_ATTRIBUTES.indexOf(dataAttr) !== -1) { - delete dataAttributes[dataAttr]; - } - }); - config = _extends({}, this.constructor.Default, dataAttributes, typeof config === 'object' && config ? config : {}); - - if (typeof config.delay === 'number') { - config.delay = { - show: config.delay, - hide: config.delay - }; - } - - if (typeof config.title === 'number') { - config.title = config.title.toString(); - } - - if (typeof config.content === 'number') { - config.content = config.content.toString(); - } - - Util.typeCheckConfig(NAME$6, config, this.constructor.DefaultType); - - if (config.sanitize) { - config.template = sanitizeHtml(config.template, config.whiteList, config.sanitizeFn); - } - - return config; - }; - - _proto._getDelegateConfig = function _getDelegateConfig() { - var config = {}; - - if (this.config) { - for (var key in this.config) { - if (this.constructor.Default[key] !== this.config[key]) { - config[key] = this.config[key]; - } - } - } - - return config; - }; - - _proto._cleanTipClass = function _cleanTipClass() { - var $tip = $__default['default'](this.getTipElement()); - var tabClass = $tip.attr('class').match(BSCLS_PREFIX_REGEX); - - if (tabClass !== null && tabClass.length) { - $tip.removeClass(tabClass.join('')); - } - }; - - _proto._handlePopperPlacementChange = function _handlePopperPlacementChange(popperData) { - this.tip = popperData.instance.popper; - - this._cleanTipClass(); - - this.addAttachmentClass(this._getAttachment(popperData.placement)); - }; - - _proto._fixTransition = function _fixTransition() { - var tip = this.getTipElement(); - var initConfigAnimation = this.config.animation; - - if (tip.getAttribute('x-placement') !== null) { - return; - } - - $__default['default'](tip).removeClass(CLASS_NAME_FADE$2); - this.config.animation = false; - this.hide(); - this.show(); - this.config.animation = initConfigAnimation; - } // Static - ; - - Tooltip._jQueryInterface = function _jQueryInterface(config) { - return this.each(function () { - var $element = $__default['default'](this); - var data = $element.data(DATA_KEY$6); - - var _config = typeof config === 'object' && config; - - if (!data && /dispose|hide/.test(config)) { - return; - } - - if (!data) { - data = new Tooltip(this, _config); - $element.data(DATA_KEY$6, data); - } - - if (typeof config === 'string') { - if (typeof data[config] === 'undefined') { - throw new TypeError("No method named \"" + config + "\""); - } - - data[config](); - } - }); - }; - - _createClass(Tooltip, null, [{ - key: "VERSION", - get: function get() { - return VERSION$6; - } - }, { - key: "Default", - get: function get() { - return Default$4; - } - }, { - key: "NAME", - get: function get() { - return NAME$6; - } - }, { - key: "DATA_KEY", - get: function get() { - return DATA_KEY$6; - } - }, { - key: "Event", - get: function get() { - return Event; - } - }, { - key: "EVENT_KEY", - get: function get() { - return EVENT_KEY$6; - } - }, { - key: "DefaultType", - get: function get() { - return DefaultType$4; - } - }]); - - return Tooltip; - }(); - /** - * ------------------------------------------------------------------------ - * jQuery - * ------------------------------------------------------------------------ - */ - - - $__default['default'].fn[NAME$6] = Tooltip._jQueryInterface; - $__default['default'].fn[NAME$6].Constructor = Tooltip; - - $__default['default'].fn[NAME$6].noConflict = function () { - $__default['default'].fn[NAME$6] = JQUERY_NO_CONFLICT$6; - return Tooltip._jQueryInterface; - }; - - /** - * ------------------------------------------------------------------------ - * Constants - * ------------------------------------------------------------------------ - */ - - var NAME$7 = 'popover'; - var VERSION$7 = '4.5.3'; - var DATA_KEY$7 = 'bs.popover'; - var EVENT_KEY$7 = "." + DATA_KEY$7; - var JQUERY_NO_CONFLICT$7 = $__default['default'].fn[NAME$7]; - var CLASS_PREFIX$1 = 'bs-popover'; - var BSCLS_PREFIX_REGEX$1 = new RegExp("(^|\\s)" + CLASS_PREFIX$1 + "\\S+", 'g'); - - var Default$5 = _extends({}, Tooltip.Default, { - placement: 'right', - trigger: 'click', - content: '', - template: '' - }); - - var DefaultType$5 = _extends({}, Tooltip.DefaultType, { - content: '(string|element|function)' - }); - - var CLASS_NAME_FADE$3 = 'fade'; - var CLASS_NAME_SHOW$5 = 'show'; - var SELECTOR_TITLE = '.popover-header'; - var SELECTOR_CONTENT = '.popover-body'; - var Event$1 = { - HIDE: "hide" + EVENT_KEY$7, - HIDDEN: "hidden" + EVENT_KEY$7, - SHOW: "show" + EVENT_KEY$7, - SHOWN: "shown" + EVENT_KEY$7, - INSERTED: "inserted" + EVENT_KEY$7, - CLICK: "click" + EVENT_KEY$7, - FOCUSIN: "focusin" + EVENT_KEY$7, - FOCUSOUT: "focusout" + EVENT_KEY$7, - MOUSEENTER: "mouseenter" + EVENT_KEY$7, - MOUSELEAVE: "mouseleave" + EVENT_KEY$7 - }; - /** - * ------------------------------------------------------------------------ - * Class Definition - * ------------------------------------------------------------------------ - */ - - var Popover = /*#__PURE__*/function (_Tooltip) { - _inheritsLoose(Popover, _Tooltip); - - function Popover() { - return _Tooltip.apply(this, arguments) || this; - } - - var _proto = Popover.prototype; - - // Overrides - _proto.isWithContent = function isWithContent() { - return this.getTitle() || this._getContent(); - }; - - _proto.addAttachmentClass = function addAttachmentClass(attachment) { - $__default['default'](this.getTipElement()).addClass(CLASS_PREFIX$1 + "-" + attachment); - }; - - _proto.getTipElement = function getTipElement() { - this.tip = this.tip || $__default['default'](this.config.template)[0]; - return this.tip; - }; - - _proto.setContent = function setContent() { - var $tip = $__default['default'](this.getTipElement()); // We use append for html objects to maintain js events - - this.setElementContent($tip.find(SELECTOR_TITLE), this.getTitle()); - - var content = this._getContent(); - - if (typeof content === 'function') { - content = content.call(this.element); - } - - this.setElementContent($tip.find(SELECTOR_CONTENT), content); - $tip.removeClass(CLASS_NAME_FADE$3 + " " + CLASS_NAME_SHOW$5); - } // Private - ; - - _proto._getContent = function _getContent() { - return this.element.getAttribute('data-content') || this.config.content; - }; - - _proto._cleanTipClass = function _cleanTipClass() { - var $tip = $__default['default'](this.getTipElement()); - var tabClass = $tip.attr('class').match(BSCLS_PREFIX_REGEX$1); - - if (tabClass !== null && tabClass.length > 0) { - $tip.removeClass(tabClass.join('')); - } - } // Static - ; - - Popover._jQueryInterface = function _jQueryInterface(config) { - return this.each(function () { - var data = $__default['default'](this).data(DATA_KEY$7); - - var _config = typeof config === 'object' ? config : null; - - if (!data && /dispose|hide/.test(config)) { - return; - } - - if (!data) { - data = new Popover(this, _config); - $__default['default'](this).data(DATA_KEY$7, data); - } - - if (typeof config === 'string') { - if (typeof data[config] === 'undefined') { - throw new TypeError("No method named \"" + config + "\""); - } - - data[config](); - } - }); - }; - - _createClass(Popover, null, [{ - key: "VERSION", - // Getters - get: function get() { - return VERSION$7; - } - }, { - key: "Default", - get: function get() { - return Default$5; - } - }, { - key: "NAME", - get: function get() { - return NAME$7; - } - }, { - key: "DATA_KEY", - get: function get() { - return DATA_KEY$7; - } - }, { - key: "Event", - get: function get() { - return Event$1; - } - }, { - key: "EVENT_KEY", - get: function get() { - return EVENT_KEY$7; - } - }, { - key: "DefaultType", - get: function get() { - return DefaultType$5; - } - }]); - - return Popover; - }(Tooltip); - /** - * ------------------------------------------------------------------------ - * jQuery - * ------------------------------------------------------------------------ - */ - - - $__default['default'].fn[NAME$7] = Popover._jQueryInterface; - $__default['default'].fn[NAME$7].Constructor = Popover; - - $__default['default'].fn[NAME$7].noConflict = function () { - $__default['default'].fn[NAME$7] = JQUERY_NO_CONFLICT$7; - return Popover._jQueryInterface; - }; - - /** - * ------------------------------------------------------------------------ - * Constants - * ------------------------------------------------------------------------ - */ - - var NAME$8 = 'scrollspy'; - var VERSION$8 = '4.5.3'; - var DATA_KEY$8 = 'bs.scrollspy'; - var EVENT_KEY$8 = "." + DATA_KEY$8; - var DATA_API_KEY$6 = '.data-api'; - var JQUERY_NO_CONFLICT$8 = $__default['default'].fn[NAME$8]; - var Default$6 = { - offset: 10, - method: 'auto', - target: '' - }; - var DefaultType$6 = { - offset: 'number', - method: 'string', - target: '(string|element)' - }; - var EVENT_ACTIVATE = "activate" + EVENT_KEY$8; - var EVENT_SCROLL = "scroll" + EVENT_KEY$8; - var EVENT_LOAD_DATA_API$2 = "load" + EVENT_KEY$8 + DATA_API_KEY$6; - var CLASS_NAME_DROPDOWN_ITEM = 'dropdown-item'; - var CLASS_NAME_ACTIVE$2 = 'active'; - var SELECTOR_DATA_SPY = '[data-spy="scroll"]'; - var SELECTOR_NAV_LIST_GROUP = '.nav, .list-group'; - var SELECTOR_NAV_LINKS = '.nav-link'; - var SELECTOR_NAV_ITEMS = '.nav-item'; - var SELECTOR_LIST_ITEMS = '.list-group-item'; - var SELECTOR_DROPDOWN = '.dropdown'; - var SELECTOR_DROPDOWN_ITEMS = '.dropdown-item'; - var SELECTOR_DROPDOWN_TOGGLE = '.dropdown-toggle'; - var METHOD_OFFSET = 'offset'; - var METHOD_POSITION = 'position'; - /** - * ------------------------------------------------------------------------ - * Class Definition - * ------------------------------------------------------------------------ - */ - - var ScrollSpy = /*#__PURE__*/function () { - function ScrollSpy(element, config) { - var _this = this; - - this._element = element; - this._scrollElement = element.tagName === 'BODY' ? window : element; - this._config = this._getConfig(config); - this._selector = this._config.target + " " + SELECTOR_NAV_LINKS + "," + (this._config.target + " " + SELECTOR_LIST_ITEMS + ",") + (this._config.target + " " + SELECTOR_DROPDOWN_ITEMS); - this._offsets = []; - this._targets = []; - this._activeTarget = null; - this._scrollHeight = 0; - $__default['default'](this._scrollElement).on(EVENT_SCROLL, function (event) { - return _this._process(event); - }); - this.refresh(); - - this._process(); - } // Getters - - - var _proto = ScrollSpy.prototype; - - // Public - _proto.refresh = function refresh() { - var _this2 = this; - - var autoMethod = this._scrollElement === this._scrollElement.window ? METHOD_OFFSET : METHOD_POSITION; - var offsetMethod = this._config.method === 'auto' ? autoMethod : this._config.method; - var offsetBase = offsetMethod === METHOD_POSITION ? this._getScrollTop() : 0; - this._offsets = []; - this._targets = []; - this._scrollHeight = this._getScrollHeight(); - var targets = [].slice.call(document.querySelectorAll(this._selector)); - targets.map(function (element) { - var target; - var targetSelector = Util.getSelectorFromElement(element); - - if (targetSelector) { - target = document.querySelector(targetSelector); - } - - if (target) { - var targetBCR = target.getBoundingClientRect(); - - if (targetBCR.width || targetBCR.height) { - // TODO (fat): remove sketch reliance on jQuery position/offset - return [$__default['default'](target)[offsetMethod]().top + offsetBase, targetSelector]; - } - } - - return null; - }).filter(function (item) { - return item; - }).sort(function (a, b) { - return a[0] - b[0]; - }).forEach(function (item) { - _this2._offsets.push(item[0]); - - _this2._targets.push(item[1]); - }); - }; - - _proto.dispose = function dispose() { - $__default['default'].removeData(this._element, DATA_KEY$8); - $__default['default'](this._scrollElement).off(EVENT_KEY$8); - this._element = null; - this._scrollElement = null; - this._config = null; - this._selector = null; - this._offsets = null; - this._targets = null; - this._activeTarget = null; - this._scrollHeight = null; - } // Private - ; - - _proto._getConfig = function _getConfig(config) { - config = _extends({}, Default$6, typeof config === 'object' && config ? config : {}); - - if (typeof config.target !== 'string' && Util.isElement(config.target)) { - var id = $__default['default'](config.target).attr('id'); - - if (!id) { - id = Util.getUID(NAME$8); - $__default['default'](config.target).attr('id', id); - } - - config.target = "#" + id; - } - - Util.typeCheckConfig(NAME$8, config, DefaultType$6); - return config; - }; - - _proto._getScrollTop = function _getScrollTop() { - return this._scrollElement === window ? this._scrollElement.pageYOffset : this._scrollElement.scrollTop; - }; - - _proto._getScrollHeight = function _getScrollHeight() { - return this._scrollElement.scrollHeight || Math.max(document.body.scrollHeight, document.documentElement.scrollHeight); - }; - - _proto._getOffsetHeight = function _getOffsetHeight() { - return this._scrollElement === window ? window.innerHeight : this._scrollElement.getBoundingClientRect().height; - }; - - _proto._process = function _process() { - var scrollTop = this._getScrollTop() + this._config.offset; - - var scrollHeight = this._getScrollHeight(); - - var maxScroll = this._config.offset + scrollHeight - this._getOffsetHeight(); - - if (this._scrollHeight !== scrollHeight) { - this.refresh(); - } - - if (scrollTop >= maxScroll) { - var target = this._targets[this._targets.length - 1]; - - if (this._activeTarget !== target) { - this._activate(target); - } - - return; - } - - if (this._activeTarget && scrollTop < this._offsets[0] && this._offsets[0] > 0) { - this._activeTarget = null; - - this._clear(); - - return; - } - - for (var i = this._offsets.length; i--;) { - var isActiveTarget = this._activeTarget !== this._targets[i] && scrollTop >= this._offsets[i] && (typeof this._offsets[i + 1] === 'undefined' || scrollTop < this._offsets[i + 1]); - - if (isActiveTarget) { - this._activate(this._targets[i]); - } - } - }; - - _proto._activate = function _activate(target) { - this._activeTarget = target; - - this._clear(); - - var queries = this._selector.split(',').map(function (selector) { - return selector + "[data-target=\"" + target + "\"]," + selector + "[href=\"" + target + "\"]"; - }); - - var $link = $__default['default']([].slice.call(document.querySelectorAll(queries.join(',')))); - - if ($link.hasClass(CLASS_NAME_DROPDOWN_ITEM)) { - $link.closest(SELECTOR_DROPDOWN).find(SELECTOR_DROPDOWN_TOGGLE).addClass(CLASS_NAME_ACTIVE$2); - $link.addClass(CLASS_NAME_ACTIVE$2); - } else { - // Set triggered link as active - $link.addClass(CLASS_NAME_ACTIVE$2); // Set triggered links parents as active - // With both
    + ` +} diff --git a/webroot/js/members.js b/webroot/js/members.js new file mode 100644 index 0000000..57c0080 --- /dev/null +++ b/webroot/js/members.js @@ -0,0 +1,205 @@ +/** + * COmanage Registry GrouperLite Members Widget Component JavaScript - Dialog Box + * + * Portions licensed to the University Corporation for Advanced Internet + * Development, Inc. ("UCAID") under one or more contributor license agreements. + * See the NOTICE file distributed with this work for additional information + * regarding copyright ownership. + * + * UCAID licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @link https://www.internet2.edu/comanage COmanage Project + * @package registry + * @since COmanage Registry v4.1.1 + * @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + */ + +import Autocomplete from './autocomplete.js'; + +export default { + props: { + add: Boolean, + remove: Boolean, + default: false + }, + inject: ['txt', 'api'], + components: { + Autocomplete + }, + data () { + return { + group: null, + loading: false, + search: '', + subscribers: [], + disabled: [], + error: null, + }; + }, + computed: { + canAdd() { + if (!this.error) { return this.add; } + if (this.error) { + const hasAccess = this.error.status !== 403; + return hasAccess && this.add; + } + } + }, + methods: { + show(group) { + this.group = group; + jQuery(this.$el).modal('show'); + this.loadGroupSubscribers(group); + }, + async loadGroupSubscribers({ name }) { + this.loading = true; + this.error = null; + const resp = await fetch(`${this.api.group}?groupname=${encodeURIComponent(name)}`, { + headers: { + "Accept": "application/json", + // 'Content-Type': 'application/x-www-form-urlencoded', + }, + method: "GET" + }); + if (resp.ok) { + this.subscribers = await resp.json(); + } else { + this.error = resp; + } + this.loading = false; + + }, + async removeSubscriber(group, subscriber) { + const {name, friendlyName} = group; + const {id, name: subName, label} = subscriber; + this.loading = true; + const resp = await fetch(`${this.api.remove}?group=${(name)}&userId=${id}`, { + method: "DELETE", + headers: { + "Accept": "application/json", + // 'Content-Type': 'application/x-www-form-urlencoded', + } + }); + if (resp.ok) { + this.subscribers = []; + this.loadGroupSubscribers(this.group); + generateFlash(`${label || subName} ${this.txt.removeSubscriberSuccess} ${(friendlyName)}`, 'success'); + } else { + this.disabled = [ ...this.disabled, id ]; + generateFlash(this.txt.removeSubscriberError, 'error'); + } + + this.loading = false; + }, + async addSubscriber(user) { + const { identifier: id, label } = user; + this.loading = true; + const { friendlyName, name } = this.group; + const formData = new FormData(); + formData.append("userId", id); + formData.append("group", name); + const resp = await fetch(`${this.api.add}?group=${name}&userId=${id}`, { + method: "POST", + headers: { + "Accept": "application/json", + // 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: formData + }); + if (resp.ok) { + this.loadGroupSubscribers(this.group); + generateFlash(`${label} ${this.txt.addSubscriberSuccess} ${(friendlyName)}`, 'success'); + } else { + generateFlash(this.txt.addSubscriberError, 'error'); + } + + this.loading = false; + } + }, + beforeMount() { + jQuery(this.$el).modal(); + }, + template: /*html*/` + + ` + } \ No newline at end of file diff --git a/webroot/js/nested-table.js b/webroot/js/nested-table.js new file mode 100644 index 0000000..ce04032 --- /dev/null +++ b/webroot/js/nested-table.js @@ -0,0 +1,70 @@ +import Popover from './popover.js'; +import Loader from './loader.js'; +import Table from './table.js'; +import Collapse from './collapse.js'; + +export default { + components: { + Loader, + Collapse, + }, + mixins: [ + Table + ], + props: { + nested: { + type: String, + default: '' + } + }, + inject: ['txt', 'owner', 'grouperUrl', 'collapsed'], + directives: { + Popover + }, + data() { + + }, + computed: { + }, + created() {}, + template: /*html*/` + + + + + + + + + +
    {{ txt.columns[column] }}
    + ` +} \ No newline at end of file diff --git a/webroot/js/page/GroupMember.js b/webroot/js/page/GroupMember.js new file mode 100644 index 0000000..a6f781b --- /dev/null +++ b/webroot/js/page/GroupMember.js @@ -0,0 +1,82 @@ +import Groups from '../groups.js'; +import PageCount from '../pagecount.js'; +import Pagination from '../pagination.js'; +import GroupsTable from '../groups-table.js'; +import NestedTable from '../nested-table.js'; +import Members from '../members.js'; +import { getQueryParam, hasQueryParam, setQueryParam } from '../params.js'; + +export default { + components: { + Groups, + PageCount, + Pagination, + GroupsTable, + Members, + NestedTable, + }, + inject: ['txt', 'api', 'owner'], + data() { + return { + view: null, + } + }, + methods: { + showSubscribers(group) { + this.$refs.members.show(group); + }, + }, + watch: { + view(newValue) { + setQueryParam('view', newValue); + } + }, + computed: { + routePath() { + return `/groupmember/co:${this.api.co}/glid:${this.api.glid}`; + } + }, + mounted() { + let view = hasQueryParam('view') ? getQueryParam('view') : 'adhoc'; + if (view !== 'working' && view !== 'adhoc') { + view = 'adhoc'; + } + this.view = view; + }, + template: /*html*/` + + + + + ` +} \ No newline at end of file diff --git a/webroot/js/page/GroupOptin.js b/webroot/js/page/GroupOptin.js new file mode 100644 index 0000000..c582c21 --- /dev/null +++ b/webroot/js/page/GroupOptin.js @@ -0,0 +1,30 @@ +import Groups from '../groups.js'; +import PageCount from '../pagecount.js'; +import Pagination from '../pagination.js'; +import GroupsTable from '../groups-table.js'; + +export default { + components: { + Groups, + PageCount, + Pagination, + GroupsTable + }, + inject: ['api'], + methods: {}, + template: /*html*/` + + + + ` +} \ No newline at end of file diff --git a/webroot/js/page/GroupOwner.js b/webroot/js/page/GroupOwner.js new file mode 100644 index 0000000..d330043 --- /dev/null +++ b/webroot/js/page/GroupOwner.js @@ -0,0 +1,38 @@ +import Groups from '../groups.js'; +import PageCount from '../pagecount.js'; +import Pagination from '../pagination.js'; +import GroupsTable from '../groups-table.js'; +import Members from '../members.js'; + +export default { + components: { + Groups, + PageCount, + Pagination, + GroupsTable, + Members + }, + inject: ['api'], + methods: { + showSubscribers(group) { + this.$refs.members.show(group); + }, + }, + template: /*html*/` + + + + + ` +} \ No newline at end of file diff --git a/webroot/js/pagecount.js b/webroot/js/pagecount.js new file mode 100644 index 0000000..1b41421 --- /dev/null +++ b/webroot/js/pagecount.js @@ -0,0 +1,29 @@ +export default { + props: { + first: { + type: Number, + default: 0 + }, + last: { + type: Number, + default: 10 + }, + total: { + type: Number, + default: 100 + } + }, + inject: ['txt'], + template: /*html*/` +
    +
    + + +
    +
    + ` +} diff --git a/webroot/js/pagination.js b/webroot/js/pagination.js new file mode 100644 index 0000000..e0babab --- /dev/null +++ b/webroot/js/pagination.js @@ -0,0 +1,126 @@ +import { getQueryParam, hasQueryParam, setQueryParam } from "./params.js"; + +export default { + props: { + perPageOptions: { + type: Array, + default: [10, 20, 50, 'all'] + }, + defaultPerPage: { + type: Number, + default: 10 + }, + page: { + type: Number, + default: 1 + }, + records: { + type: Array, + default: [], + } + }, + inject: ['txt'], + data() { + return { + currentPage: hasQueryParam('page') ? getQueryParam('page') : 1, + perPage: hasQueryParam('limit') ? getQueryParam('limit') : 10, + } + }, + watch: { + currentPage(newValue) { + setQueryParam('page', newValue); + }, + perPage(newValue) { + setQueryParam('limit', newValue); + if (newValue === 'all') { + this.currentPage = 1; + } + }, + records(newValue) { + if (this.numbers <= 1) { + this.currentPage = 1; + } + } + }, + computed: { + size() { + let max = this.perPage; + if (this.perPage === 'all') { + max = this.records.length; + } + return max; + }, + start() { + return (this.currentPage - 1) * this.size; + }, + end() { + const last = (this.start + this.size); + return last >= this.records.length ? this.records.length : last; + }, + numbers() { + return Math.ceil(this.records.length / this.size); + }, + currentList() { + return this.records.slice(this.start, this.end); + }, + first() { + return 1; + }, + last() { + return this.numbers; + } + }, + mounted() { + this.currentPage = this.page; + }, + template: /*html*/` + + + ` +} + + diff --git a/webroot/js/params.js b/webroot/js/params.js new file mode 100644 index 0000000..b44b325 --- /dev/null +++ b/webroot/js/params.js @@ -0,0 +1,24 @@ +export function getQueryParams() { + return new URLSearchParams(window.location.search); +} + +export function getQueryParam(name = '', list = false) { + const params = getQueryParams(); + return params.has(name) ? list ? params.getAll(name) : params.get(name) : null; +} + +export function hasQueryParam(name) { + const params = getQueryParams(); + return params.has(name); +} + +export function setQueryParam(name, value, remove = false) { + const params = getQueryParams(); + if (!value && remove) { + params.delete(name); + } else { + params.set(name, value); + } + const p = params.toString().length > 0 ? `?${params.toString()}` : '' + window.history.replaceState({}, "", decodeURIComponent(`${window.location.pathname}${p}`)); +} \ No newline at end of file diff --git a/webroot/js/popover.js b/webroot/js/popover.js new file mode 100644 index 0000000..68ac519 --- /dev/null +++ b/webroot/js/popover.js @@ -0,0 +1,10 @@ +export default { + mounted(el, { value }) { + jQuery(el).popover({ + content: value, + trigger: 'hover', + placement: 'top', + container: 'body' + }) + } +} \ No newline at end of file diff --git a/webroot/js/table.js b/webroot/js/table.js new file mode 100644 index 0000000..87e868d --- /dev/null +++ b/webroot/js/table.js @@ -0,0 +1,39 @@ +import Popover from './popover.js'; +import Loader from './loader.js'; + +export default { + components: { + Loader + }, + props: { + columns: { + type: Array, + default: ['name', 'role', 'description', 'status', 'action'] + }, + groups: { + type: Array, + default: [] + }, + members: { + type: Boolean, + default: false, + }, + grouper: { + type: Boolean, + default: false, + } + }, + inject: ['txt', 'owner', 'grouperUrl'], + directives: { + Popover + }, + methods: { + showOptAction(group) { + if (this.$attrs.onLeaveGroup){ + return group.optOut; + } else { + return this.$attrs.onJoinGroup !== null; + } + }, + }, +} \ No newline at end of file diff --git a/webroot/js/tabs/tab.js b/webroot/js/tabs/tab.js new file mode 100644 index 0000000..3d95a20 --- /dev/null +++ b/webroot/js/tabs/tab.js @@ -0,0 +1,18 @@ +export default { + props: { + title: { + type: String, + default: 'Tab' + }, + name: { + type: String, + default: 'groupmember' + }, + }, + inject: ['txt', 'active'], + template: /*html*/` +
    + +
    + ` +} \ No newline at end of file diff --git a/webroot/js/tabs/tabs.js b/webroot/js/tabs/tabs.js new file mode 100644 index 0000000..2229b01 --- /dev/null +++ b/webroot/js/tabs/tabs.js @@ -0,0 +1,46 @@ +const { computed } = window.Vue; + +export default { + props: { + defaultTab: { + type: String, + default: 'Tab' + } + }, + data () { + return { + selected: '', + tabs: [], + } + }, + provide() { + return { + active: computed(() => this.selected), + } + }, + created () { + const slots = this.$slots.default(); + this.titles = slots.map(t => t.props.title); + this.tabs = slots.map(t => t.props.name); + this.selectTab(this.defaultTab); + }, + methods: { + selectTab (i) { + if (i !== this.selected) { + this.selected = i; + this.$emit('change', i); + } + } + }, + template: /*html*/` + + + ` +} + diff --git a/webroot/js/typeahead.bundle.js b/webroot/js/typeahead.bundle.js deleted file mode 100644 index bb0c8ae..0000000 --- a/webroot/js/typeahead.bundle.js +++ /dev/null @@ -1,2451 +0,0 @@ -/*! - * typeahead.js 0.11.1 - * https://github.com/twitter/typeahead.js - * Copyright 2013-2015 Twitter, Inc. and other contributors; Licensed MIT - */ - -(function(root, factory) { - if (typeof define === "function" && define.amd) { - define("bloodhound", [ "jquery" ], function(a0) { - return root["Bloodhound"] = factory(a0); - }); - } else if (typeof exports === "object") { - module.exports = factory(require("jquery")); - } else { - root["Bloodhound"] = factory(jQuery); - } -})(this, function($) { - var _ = function() { - "use strict"; - return { - isMsie: function() { - return /(msie|trident)/i.test(navigator.userAgent) ? navigator.userAgent.match(/(msie |rv:)(\d+(.\d+)?)/i)[2] : false; - }, - isBlankString: function(str) { - return !str || /^\s*$/.test(str); - }, - escapeRegExChars: function(str) { - return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); - }, - isString: function(obj) { - return typeof obj === "string"; - }, - isNumber: function(obj) { - return typeof obj === "number"; - }, - isArray: $.isArray, - isFunction: $.isFunction, - isObject: $.isPlainObject, - isUndefined: function(obj) { - return typeof obj === "undefined"; - }, - isElement: function(obj) { - return !!(obj && obj.nodeType === 1); - }, - isJQuery: function(obj) { - return obj instanceof $; - }, - toStr: function toStr(s) { - return _.isUndefined(s) || s === null ? "" : s + ""; - }, - bind: $.proxy, - each: function(collection, cb) { - $.each(collection, reverseArgs); - function reverseArgs(index, value) { - return cb(value, index); - } - }, - map: $.map, - filter: $.grep, - every: function(obj, test) { - var result = true; - if (!obj) { - return result; - } - $.each(obj, function(key, val) { - if (!(result = test.call(null, val, key, obj))) { - return false; - } - }); - return !!result; - }, - some: function(obj, test) { - var result = false; - if (!obj) { - return result; - } - $.each(obj, function(key, val) { - if (result = test.call(null, val, key, obj)) { - return false; - } - }); - return !!result; - }, - mixin: $.extend, - identity: function(x) { - return x; - }, - clone: function(obj) { - return $.extend(true, {}, obj); - }, - getIdGenerator: function() { - var counter = 0; - return function() { - return counter++; - }; - }, - templatify: function templatify(obj) { - return $.isFunction(obj) ? obj : template; - function template() { - return String(obj); - } - }, - defer: function(fn) { - setTimeout(fn, 0); - }, - debounce: function(func, wait, immediate) { - var timeout, result; - return function() { - var context = this, args = arguments, later, callNow; - later = function() { - timeout = null; - if (!immediate) { - result = func.apply(context, args); - } - }; - callNow = immediate && !timeout; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - if (callNow) { - result = func.apply(context, args); - } - return result; - }; - }, - throttle: function(func, wait) { - var context, args, timeout, result, previous, later; - previous = 0; - later = function() { - previous = new Date(); - timeout = null; - result = func.apply(context, args); - }; - return function() { - var now = new Date(), remaining = wait - (now - previous); - context = this; - args = arguments; - if (remaining <= 0) { - clearTimeout(timeout); - timeout = null; - previous = now; - result = func.apply(context, args); - } else if (!timeout) { - timeout = setTimeout(later, remaining); - } - return result; - }; - }, - stringify: function(val) { - return _.isString(val) ? val : JSON.stringify(val); - }, - noop: function() {} - }; - }(); - var VERSION = "0.11.1"; - var tokenizers = function() { - "use strict"; - return { - nonword: nonword, - whitespace: whitespace, - obj: { - nonword: getObjTokenizer(nonword), - whitespace: getObjTokenizer(whitespace) - } - }; - function whitespace(str) { - str = _.toStr(str); - return str ? str.split(/\s+/) : []; - } - function nonword(str) { - str = _.toStr(str); - return str ? str.split(/\W+/) : []; - } - function getObjTokenizer(tokenizer) { - return function setKey(keys) { - keys = _.isArray(keys) ? keys : [].slice.call(arguments, 0); - return function tokenize(o) { - var tokens = []; - _.each(keys, function(k) { - tokens = tokens.concat(tokenizer(_.toStr(o[k]))); - }); - return tokens; - }; - }; - } - }(); - var LruCache = function() { - "use strict"; - function LruCache(maxSize) { - this.maxSize = _.isNumber(maxSize) ? maxSize : 100; - this.reset(); - if (this.maxSize <= 0) { - this.set = this.get = $.noop; - } - } - _.mixin(LruCache.prototype, { - set: function set(key, val) { - var tailItem = this.list.tail, node; - if (this.size >= this.maxSize) { - this.list.remove(tailItem); - delete this.hash[tailItem.key]; - this.size--; - } - if (node = this.hash[key]) { - node.val = val; - this.list.moveToFront(node); - } else { - node = new Node(key, val); - this.list.add(node); - this.hash[key] = node; - this.size++; - } - }, - get: function get(key) { - var node = this.hash[key]; - if (node) { - this.list.moveToFront(node); - return node.val; - } - }, - reset: function reset() { - this.size = 0; - this.hash = {}; - this.list = new List(); - } - }); - function List() { - this.head = this.tail = null; - } - _.mixin(List.prototype, { - add: function add(node) { - if (this.head) { - node.next = this.head; - this.head.prev = node; - } - this.head = node; - this.tail = this.tail || node; - }, - remove: function remove(node) { - node.prev ? node.prev.next = node.next : this.head = node.next; - node.next ? node.next.prev = node.prev : this.tail = node.prev; - }, - moveToFront: function(node) { - this.remove(node); - this.add(node); - } - }); - function Node(key, val) { - this.key = key; - this.val = val; - this.prev = this.next = null; - } - return LruCache; - }(); - var PersistentStorage = function() { - "use strict"; - var LOCAL_STORAGE; - try { - LOCAL_STORAGE = window.localStorage; - LOCAL_STORAGE.setItem("~~~", "!"); - LOCAL_STORAGE.removeItem("~~~"); - } catch (err) { - LOCAL_STORAGE = null; - } - function PersistentStorage(namespace, override) { - this.prefix = [ "__", namespace, "__" ].join(""); - this.ttlKey = "__ttl__"; - this.keyMatcher = new RegExp("^" + _.escapeRegExChars(this.prefix)); - this.ls = override || LOCAL_STORAGE; - !this.ls && this._noop(); - } - _.mixin(PersistentStorage.prototype, { - _prefix: function(key) { - return this.prefix + key; - }, - _ttlKey: function(key) { - return this._prefix(key) + this.ttlKey; - }, - _noop: function() { - this.get = this.set = this.remove = this.clear = this.isExpired = _.noop; - }, - _safeSet: function(key, val) { - try { - this.ls.setItem(key, val); - } catch (err) { - if (err.name === "QuotaExceededError") { - this.clear(); - this._noop(); - } - } - }, - get: function(key) { - if (this.isExpired(key)) { - this.remove(key); - } - return decode(this.ls.getItem(this._prefix(key))); - }, - set: function(key, val, ttl) { - if (_.isNumber(ttl)) { - this._safeSet(this._ttlKey(key), encode(now() + ttl)); - } else { - this.ls.removeItem(this._ttlKey(key)); - } - return this._safeSet(this._prefix(key), encode(val)); - }, - remove: function(key) { - this.ls.removeItem(this._ttlKey(key)); - this.ls.removeItem(this._prefix(key)); - return this; - }, - clear: function() { - var i, keys = gatherMatchingKeys(this.keyMatcher); - for (i = keys.length; i--; ) { - this.remove(keys[i]); - } - return this; - }, - isExpired: function(key) { - var ttl = decode(this.ls.getItem(this._ttlKey(key))); - return _.isNumber(ttl) && now() > ttl ? true : false; - } - }); - return PersistentStorage; - function now() { - return new Date().getTime(); - } - function encode(val) { - return JSON.stringify(_.isUndefined(val) ? null : val); - } - function decode(val) { - return $.parseJSON(val); - } - function gatherMatchingKeys(keyMatcher) { - var i, key, keys = [], len = LOCAL_STORAGE.length; - for (i = 0; i < len; i++) { - if ((key = LOCAL_STORAGE.key(i)).match(keyMatcher)) { - keys.push(key.replace(keyMatcher, "")); - } - } - return keys; - } - }(); - var Transport = function() { - "use strict"; - var pendingRequestsCount = 0, pendingRequests = {}, maxPendingRequests = 6, sharedCache = new LruCache(10); - function Transport(o) { - o = o || {}; - this.cancelled = false; - this.lastReq = null; - this._send = o.transport; - this._get = o.limiter ? o.limiter(this._get) : this._get; - this._cache = o.cache === false ? new LruCache(0) : sharedCache; - } - Transport.setMaxPendingRequests = function setMaxPendingRequests(num) { - maxPendingRequests = num; - }; - Transport.resetCache = function resetCache() { - sharedCache.reset(); - }; - _.mixin(Transport.prototype, { - _fingerprint: function fingerprint(o) { - o = o || {}; - return o.url + o.type + $.param(o.data || {}); - }, - _get: function(o, cb) { - var that = this, fingerprint, jqXhr; - fingerprint = this._fingerprint(o); - if (this.cancelled || fingerprint !== this.lastReq) { - return; - } - if (jqXhr = pendingRequests[fingerprint]) { - jqXhr.done(done).fail(fail); - } else if (pendingRequestsCount < maxPendingRequests) { - pendingRequestsCount++; - pendingRequests[fingerprint] = this._send(o).done(done).fail(fail).always(always); - } else { - this.onDeckRequestArgs = [].slice.call(arguments, 0); - } - function done(resp) { - cb(null, resp); - that._cache.set(fingerprint, resp); - } - function fail() { - cb(true); - } - function always() { - pendingRequestsCount--; - delete pendingRequests[fingerprint]; - if (that.onDeckRequestArgs) { - that._get.apply(that, that.onDeckRequestArgs); - that.onDeckRequestArgs = null; - } - } - }, - get: function(o, cb) { - var resp, fingerprint; - cb = cb || $.noop; - o = _.isString(o) ? { - url: o - } : o || {}; - fingerprint = this._fingerprint(o); - this.cancelled = false; - this.lastReq = fingerprint; - if (resp = this._cache.get(fingerprint)) { - cb(null, resp); - } else { - this._get(o, cb); - } - }, - cancel: function() { - this.cancelled = true; - } - }); - return Transport; - }(); - var SearchIndex = window.SearchIndex = function() { - "use strict"; - var CHILDREN = "c", IDS = "i"; - function SearchIndex(o) { - o = o || {}; - if (!o.datumTokenizer || !o.queryTokenizer) { - $.error("datumTokenizer and queryTokenizer are both required"); - } - this.identify = o.identify || _.stringify; - this.datumTokenizer = o.datumTokenizer; - this.queryTokenizer = o.queryTokenizer; - this.reset(); - } - _.mixin(SearchIndex.prototype, { - bootstrap: function bootstrap(o) { - this.datums = o.datums; - this.trie = o.trie; - }, - add: function(data) { - var that = this; - data = _.isArray(data) ? data : [ data ]; - _.each(data, function(datum) { - var id, tokens; - that.datums[id = that.identify(datum)] = datum; - tokens = normalizeTokens(that.datumTokenizer(datum)); - _.each(tokens, function(token) { - var node, chars, ch; - node = that.trie; - chars = token.split(""); - while (ch = chars.shift()) { - node = node[CHILDREN][ch] || (node[CHILDREN][ch] = newNode()); - node[IDS].push(id); - } - }); - }); - }, - get: function get(ids) { - var that = this; - return _.map(ids, function(id) { - return that.datums[id]; - }); - }, - search: function search(query) { - var that = this, tokens, matches; - tokens = normalizeTokens(this.queryTokenizer(query)); - _.each(tokens, function(token) { - var node, chars, ch, ids; - if (matches && matches.length === 0) { - return false; - } - node = that.trie; - chars = token.split(""); - while (node && (ch = chars.shift())) { - node = node[CHILDREN][ch]; - } - if (node && chars.length === 0) { - ids = node[IDS].slice(0); - matches = matches ? getIntersection(matches, ids) : ids; - } else { - matches = []; - return false; - } - }); - return matches ? _.map(unique(matches), function(id) { - return that.datums[id]; - }) : []; - }, - all: function all() { - var values = []; - for (var key in this.datums) { - values.push(this.datums[key]); - } - return values; - }, - reset: function reset() { - this.datums = {}; - this.trie = newNode(); - }, - serialize: function serialize() { - return { - datums: this.datums, - trie: this.trie - }; - } - }); - return SearchIndex; - function normalizeTokens(tokens) { - tokens = _.filter(tokens, function(token) { - return !!token; - }); - tokens = _.map(tokens, function(token) { - return token.toLowerCase(); - }); - return tokens; - } - function newNode() { - var node = {}; - node[IDS] = []; - node[CHILDREN] = {}; - return node; - } - function unique(array) { - var seen = {}, uniques = []; - for (var i = 0, len = array.length; i < len; i++) { - if (!seen[array[i]]) { - seen[array[i]] = true; - uniques.push(array[i]); - } - } - return uniques; - } - function getIntersection(arrayA, arrayB) { - var ai = 0, bi = 0, intersection = []; - arrayA = arrayA.sort(); - arrayB = arrayB.sort(); - var lenArrayA = arrayA.length, lenArrayB = arrayB.length; - while (ai < lenArrayA && bi < lenArrayB) { - if (arrayA[ai] < arrayB[bi]) { - ai++; - } else if (arrayA[ai] > arrayB[bi]) { - bi++; - } else { - intersection.push(arrayA[ai]); - ai++; - bi++; - } - } - return intersection; - } - }(); - var Prefetch = function() { - "use strict"; - var keys; - keys = { - data: "data", - protocol: "protocol", - thumbprint: "thumbprint" - }; - function Prefetch(o) { - this.url = o.url; - this.ttl = o.ttl; - this.cache = o.cache; - this.prepare = o.prepare; - this.transform = o.transform; - this.transport = o.transport; - this.thumbprint = o.thumbprint; - this.storage = new PersistentStorage(o.cacheKey); - } - _.mixin(Prefetch.prototype, { - _settings: function settings() { - return { - url: this.url, - type: "GET", - dataType: "json" - }; - }, - store: function store(data) { - if (!this.cache) { - return; - } - this.storage.set(keys.data, data, this.ttl); - this.storage.set(keys.protocol, location.protocol, this.ttl); - this.storage.set(keys.thumbprint, this.thumbprint, this.ttl); - }, - fromCache: function fromCache() { - var stored = {}, isExpired; - if (!this.cache) { - return null; - } - stored.data = this.storage.get(keys.data); - stored.protocol = this.storage.get(keys.protocol); - stored.thumbprint = this.storage.get(keys.thumbprint); - isExpired = stored.thumbprint !== this.thumbprint || stored.protocol !== location.protocol; - return stored.data && !isExpired ? stored.data : null; - }, - fromNetwork: function(cb) { - var that = this, settings; - if (!cb) { - return; - } - settings = this.prepare(this._settings()); - this.transport(settings).fail(onError).done(onResponse); - function onError() { - cb(true); - } - function onResponse(resp) { - cb(null, that.transform(resp)); - } - }, - clear: function clear() { - this.storage.clear(); - return this; - } - }); - return Prefetch; - }(); - var Remote = function() { - "use strict"; - function Remote(o) { - this.url = o.url; - this.prepare = o.prepare; - this.transform = o.transform; - this.transport = new Transport({ - cache: o.cache, - limiter: o.limiter, - transport: o.transport - }); - } - _.mixin(Remote.prototype, { - _settings: function settings() { - return { - url: this.url, - type: "GET", - dataType: "json" - }; - }, - get: function get(query, cb) { - var that = this, settings; - if (!cb) { - return; - } - query = query || ""; - settings = this.prepare(query, this._settings()); - return this.transport.get(settings, onResponse); - function onResponse(err, resp) { - err ? cb([]) : cb(that.transform(resp)); - } - }, - cancelLastRequest: function cancelLastRequest() { - this.transport.cancel(); - } - }); - return Remote; - }(); - var oParser = function() { - "use strict"; - return function parse(o) { - var defaults, sorter; - defaults = { - initialize: true, - identify: _.stringify, - datumTokenizer: null, - queryTokenizer: null, - sufficient: 5, - sorter: null, - local: [], - prefetch: null, - remote: null - }; - o = _.mixin(defaults, o || {}); - !o.datumTokenizer && $.error("datumTokenizer is required"); - !o.queryTokenizer && $.error("queryTokenizer is required"); - sorter = o.sorter; - o.sorter = sorter ? function(x) { - return x.sort(sorter); - } : _.identity; - o.local = _.isFunction(o.local) ? o.local() : o.local; - o.prefetch = parsePrefetch(o.prefetch); - o.remote = parseRemote(o.remote); - return o; - }; - function parsePrefetch(o) { - var defaults; - if (!o) { - return null; - } - defaults = { - url: null, - ttl: 24 * 60 * 60 * 1e3, - cache: true, - cacheKey: null, - thumbprint: "", - prepare: _.identity, - transform: _.identity, - transport: null - }; - o = _.isString(o) ? { - url: o - } : o; - o = _.mixin(defaults, o); - !o.url && $.error("prefetch requires url to be set"); - o.transform = o.filter || o.transform; - o.cacheKey = o.cacheKey || o.url; - o.thumbprint = VERSION + o.thumbprint; - o.transport = o.transport ? callbackToDeferred(o.transport) : $.ajax; - return o; - } - function parseRemote(o) { - var defaults; - if (!o) { - return; - } - defaults = { - url: null, - cache: true, - prepare: null, - replace: null, - wildcard: null, - limiter: null, - rateLimitBy: "debounce", - rateLimitWait: 300, - transform: _.identity, - transport: null - }; - o = _.isString(o) ? { - url: o - } : o; - o = _.mixin(defaults, o); - !o.url && $.error("remote requires url to be set"); - o.transform = o.filter || o.transform; - o.prepare = toRemotePrepare(o); - o.limiter = toLimiter(o); - o.transport = o.transport ? callbackToDeferred(o.transport) : $.ajax; - delete o.replace; - delete o.wildcard; - delete o.rateLimitBy; - delete o.rateLimitWait; - return o; - } - function toRemotePrepare(o) { - var prepare, replace, wildcard; - prepare = o.prepare; - replace = o.replace; - wildcard = o.wildcard; - if (prepare) { - return prepare; - } - if (replace) { - prepare = prepareByReplace; - } else if (o.wildcard) { - prepare = prepareByWildcard; - } else { - prepare = idenityPrepare; - } - return prepare; - function prepareByReplace(query, settings) { - settings.url = replace(settings.url, query); - return settings; - } - function prepareByWildcard(query, settings) { - settings.url = settings.url.replace(wildcard, encodeURIComponent(query)); - return settings; - } - function idenityPrepare(query, settings) { - return settings; - } - } - function toLimiter(o) { - var limiter, method, wait; - limiter = o.limiter; - method = o.rateLimitBy; - wait = o.rateLimitWait; - if (!limiter) { - limiter = /^throttle$/i.test(method) ? throttle(wait) : debounce(wait); - } - return limiter; - function debounce(wait) { - return function debounce(fn) { - return _.debounce(fn, wait); - }; - } - function throttle(wait) { - return function throttle(fn) { - return _.throttle(fn, wait); - }; - } - } - function callbackToDeferred(fn) { - return function wrapper(o) { - var deferred = $.Deferred(); - fn(o, onSuccess, onError); - return deferred; - function onSuccess(resp) { - _.defer(function() { - deferred.resolve(resp); - }); - } - function onError(err) { - _.defer(function() { - deferred.reject(err); - }); - } - }; - } - }(); - var Bloodhound = function() { - "use strict"; - var old; - old = window && window.Bloodhound; - function Bloodhound(o) { - o = oParser(o); - this.sorter = o.sorter; - this.identify = o.identify; - this.sufficient = o.sufficient; - this.local = o.local; - this.remote = o.remote ? new Remote(o.remote) : null; - this.prefetch = o.prefetch ? new Prefetch(o.prefetch) : null; - this.index = new SearchIndex({ - identify: this.identify, - datumTokenizer: o.datumTokenizer, - queryTokenizer: o.queryTokenizer - }); - o.initialize !== false && this.initialize(); - } - Bloodhound.noConflict = function noConflict() { - window && (window.Bloodhound = old); - return Bloodhound; - }; - Bloodhound.tokenizers = tokenizers; - _.mixin(Bloodhound.prototype, { - __ttAdapter: function ttAdapter() { - var that = this; - return this.remote ? withAsync : withoutAsync; - function withAsync(query, sync, async) { - return that.search(query, sync, async); - } - function withoutAsync(query, sync) { - return that.search(query, sync); - } - }, - _loadPrefetch: function loadPrefetch() { - var that = this, deferred, serialized; - deferred = $.Deferred(); - if (!this.prefetch) { - deferred.resolve(); - } else if (serialized = this.prefetch.fromCache()) { - this.index.bootstrap(serialized); - deferred.resolve(); - } else { - this.prefetch.fromNetwork(done); - } - return deferred.promise(); - function done(err, data) { - if (err) { - return deferred.reject(); - } - that.add(data); - that.prefetch.store(that.index.serialize()); - deferred.resolve(); - } - }, - _initialize: function initialize() { - var that = this, deferred; - this.clear(); - (this.initPromise = this._loadPrefetch()).done(addLocalToIndex); - return this.initPromise; - function addLocalToIndex() { - that.add(that.local); - } - }, - initialize: function initialize(force) { - return !this.initPromise || force ? this._initialize() : this.initPromise; - }, - add: function add(data) { - this.index.add(data); - return this; - }, - get: function get(ids) { - ids = _.isArray(ids) ? ids : [].slice.call(arguments); - return this.index.get(ids); - }, - search: function search(query, sync, async) { - var that = this, local; - local = this.sorter(this.index.search(query)); - sync(this.remote ? local.slice() : local); - if (this.remote && local.length < this.sufficient) { - this.remote.get(query, processRemote); - } else if (this.remote) { - this.remote.cancelLastRequest(); - } - return this; - function processRemote(remote) { - var nonDuplicates = []; - _.each(remote, function(r) { - !_.some(local, function(l) { - return that.identify(r) === that.identify(l); - }) && nonDuplicates.push(r); - }); - async && async(nonDuplicates); - } - }, - all: function all() { - return this.index.all(); - }, - clear: function clear() { - this.index.reset(); - return this; - }, - clearPrefetchCache: function clearPrefetchCache() { - this.prefetch && this.prefetch.clear(); - return this; - }, - clearRemoteCache: function clearRemoteCache() { - Transport.resetCache(); - return this; - }, - ttAdapter: function ttAdapter() { - return this.__ttAdapter(); - } - }); - return Bloodhound; - }(); - return Bloodhound; -}); - -(function(root, factory) { - if (typeof define === "function" && define.amd) { - define("typeahead.js", [ "jquery" ], function(a0) { - return factory(a0); - }); - } else if (typeof exports === "object") { - module.exports = factory(require("jquery")); - } else { - factory(jQuery); - } -})(this, function($) { - var _ = function() { - "use strict"; - return { - isMsie: function() { - return /(msie|trident)/i.test(navigator.userAgent) ? navigator.userAgent.match(/(msie |rv:)(\d+(.\d+)?)/i)[2] : false; - }, - isBlankString: function(str) { - return !str || /^\s*$/.test(str); - }, - escapeRegExChars: function(str) { - return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); - }, - isString: function(obj) { - return typeof obj === "string"; - }, - isNumber: function(obj) { - return typeof obj === "number"; - }, - isArray: $.isArray, - isFunction: $.isFunction, - isObject: $.isPlainObject, - isUndefined: function(obj) { - return typeof obj === "undefined"; - }, - isElement: function(obj) { - return !!(obj && obj.nodeType === 1); - }, - isJQuery: function(obj) { - return obj instanceof $; - }, - toStr: function toStr(s) { - return _.isUndefined(s) || s === null ? "" : s + ""; - }, - bind: $.proxy, - each: function(collection, cb) { - $.each(collection, reverseArgs); - function reverseArgs(index, value) { - return cb(value, index); - } - }, - map: $.map, - filter: $.grep, - every: function(obj, test) { - var result = true; - if (!obj) { - return result; - } - $.each(obj, function(key, val) { - if (!(result = test.call(null, val, key, obj))) { - return false; - } - }); - return !!result; - }, - some: function(obj, test) { - var result = false; - if (!obj) { - return result; - } - $.each(obj, function(key, val) { - if (result = test.call(null, val, key, obj)) { - return false; - } - }); - return !!result; - }, - mixin: $.extend, - identity: function(x) { - return x; - }, - clone: function(obj) { - return $.extend(true, {}, obj); - }, - getIdGenerator: function() { - var counter = 0; - return function() { - return counter++; - }; - }, - templatify: function templatify(obj) { - return $.isFunction(obj) ? obj : template; - function template() { - return String(obj); - } - }, - defer: function(fn) { - setTimeout(fn, 0); - }, - debounce: function(func, wait, immediate) { - var timeout, result; - return function() { - var context = this, args = arguments, later, callNow; - later = function() { - timeout = null; - if (!immediate) { - result = func.apply(context, args); - } - }; - callNow = immediate && !timeout; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - if (callNow) { - result = func.apply(context, args); - } - return result; - }; - }, - throttle: function(func, wait) { - var context, args, timeout, result, previous, later; - previous = 0; - later = function() { - previous = new Date(); - timeout = null; - result = func.apply(context, args); - }; - return function() { - var now = new Date(), remaining = wait - (now - previous); - context = this; - args = arguments; - if (remaining <= 0) { - clearTimeout(timeout); - timeout = null; - previous = now; - result = func.apply(context, args); - } else if (!timeout) { - timeout = setTimeout(later, remaining); - } - return result; - }; - }, - stringify: function(val) { - return _.isString(val) ? val : JSON.stringify(val); - }, - noop: function() {} - }; - }(); - var WWW = function() { - "use strict"; - var defaultClassNames = { - wrapper: "twitter-typeahead", - input: "tt-input", - hint: "tt-hint", - menu: "tt-menu", - dataset: "tt-dataset", - suggestion: "tt-suggestion", - selectable: "tt-selectable", - empty: "tt-empty", - open: "tt-open", - cursor: "tt-cursor", - highlight: "tt-highlight" - }; - return build; - function build(o) { - var www, classes; - classes = _.mixin({}, defaultClassNames, o); - www = { - css: buildCss(), - classes: classes, - html: buildHtml(classes), - selectors: buildSelectors(classes) - }; - return { - css: www.css, - html: www.html, - classes: www.classes, - selectors: www.selectors, - mixin: function(o) { - _.mixin(o, www); - } - }; - } - function buildHtml(c) { - return { - wrapper: '', - menu: '
    ' - }; - } - function buildSelectors(classes) { - var selectors = {}; - _.each(classes, function(v, k) { - selectors[k] = "." + v; - }); - return selectors; - } - function buildCss() { - var css = { - wrapper: { - position: "relative", - display: "inline-block" - }, - hint: { - position: "absolute", - top: "0", - left: "0", - borderColor: "transparent", - boxShadow: "none", - opacity: "1" - }, - input: { - position: "relative", - verticalAlign: "top", - backgroundColor: "transparent" - }, - inputWithNoHint: { - position: "relative", - verticalAlign: "top" - }, - menu: { - position: "absolute", - top: "100%", - left: "0", - zIndex: "100", - display: "none" - }, - ltr: { - left: "0", - right: "auto" - }, - rtl: { - left: "auto", - right: " 0" - } - }; - if (_.isMsie()) { - _.mixin(css.input, { - backgroundImage: "url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)" - }); - } - return css; - } - }(); - var EventBus = function() { - "use strict"; - var namespace, deprecationMap; - namespace = "typeahead:"; - deprecationMap = { - render: "rendered", - cursorchange: "cursorchanged", - select: "selected", - autocomplete: "autocompleted" - }; - function EventBus(o) { - if (!o || !o.el) { - $.error("EventBus initialized without el"); - } - this.$el = $(o.el); - } - _.mixin(EventBus.prototype, { - _trigger: function(type, args) { - var $e; - $e = $.Event(namespace + type); - (args = args || []).unshift($e); - this.$el.trigger.apply(this.$el, args); - return $e; - }, - before: function(type) { - var args, $e; - args = [].slice.call(arguments, 1); - $e = this._trigger("before" + type, args); - return $e.isDefaultPrevented(); - }, - trigger: function(type) { - var deprecatedType; - this._trigger(type, [].slice.call(arguments, 1)); - if (deprecatedType = deprecationMap[type]) { - this._trigger(deprecatedType, [].slice.call(arguments, 1)); - } - } - }); - return EventBus; - }(); - var EventEmitter = function() { - "use strict"; - var splitter = /\s+/, nextTick = getNextTick(); - return { - onSync: onSync, - onAsync: onAsync, - off: off, - trigger: trigger - }; - function on(method, types, cb, context) { - var type; - if (!cb) { - return this; - } - types = types.split(splitter); - cb = context ? bindContext(cb, context) : cb; - this._callbacks = this._callbacks || {}; - while (type = types.shift()) { - this._callbacks[type] = this._callbacks[type] || { - sync: [], - async: [] - }; - this._callbacks[type][method].push(cb); - } - return this; - } - function onAsync(types, cb, context) { - return on.call(this, "async", types, cb, context); - } - function onSync(types, cb, context) { - return on.call(this, "sync", types, cb, context); - } - function off(types) { - var type; - if (!this._callbacks) { - return this; - } - types = types.split(splitter); - while (type = types.shift()) { - delete this._callbacks[type]; - } - return this; - } - function trigger(types) { - var type, callbacks, args, syncFlush, asyncFlush; - if (!this._callbacks) { - return this; - } - types = types.split(splitter); - args = [].slice.call(arguments, 1); - while ((type = types.shift()) && (callbacks = this._callbacks[type])) { - syncFlush = getFlush(callbacks.sync, this, [ type ].concat(args)); - asyncFlush = getFlush(callbacks.async, this, [ type ].concat(args)); - syncFlush() && nextTick(asyncFlush); - } - return this; - } - function getFlush(callbacks, context, args) { - return flush; - function flush() { - var cancelled; - for (var i = 0, len = callbacks.length; !cancelled && i < len; i += 1) { - cancelled = callbacks[i].apply(context, args) === false; - } - return !cancelled; - } - } - function getNextTick() { - var nextTickFn; - if (window.setImmediate) { - nextTickFn = function nextTickSetImmediate(fn) { - setImmediate(function() { - fn(); - }); - }; - } else { - nextTickFn = function nextTickSetTimeout(fn) { - setTimeout(function() { - fn(); - }, 0); - }; - } - return nextTickFn; - } - function bindContext(fn, context) { - return fn.bind ? fn.bind(context) : function() { - fn.apply(context, [].slice.call(arguments, 0)); - }; - } - }(); - var highlight = function(doc) { - "use strict"; - var defaults = { - node: null, - pattern: null, - tagName: "strong", - className: null, - wordsOnly: false, - caseSensitive: false - }; - return function hightlight(o) { - var regex; - o = _.mixin({}, defaults, o); - if (!o.node || !o.pattern) { - return; - } - o.pattern = _.isArray(o.pattern) ? o.pattern : [ o.pattern ]; - regex = getRegex(o.pattern, o.caseSensitive, o.wordsOnly); - traverse(o.node, hightlightTextNode); - function hightlightTextNode(textNode) { - var match, patternNode, wrapperNode; - if (match = regex.exec(textNode.data)) { - wrapperNode = doc.createElement(o.tagName); - o.className && (wrapperNode.className = o.className); - patternNode = textNode.splitText(match.index); - patternNode.splitText(match[0].length); - wrapperNode.appendChild(patternNode.cloneNode(true)); - textNode.parentNode.replaceChild(wrapperNode, patternNode); - } - return !!match; - } - function traverse(el, hightlightTextNode) { - var childNode, TEXT_NODE_TYPE = 3; - for (var i = 0; i < el.childNodes.length; i++) { - childNode = el.childNodes[i]; - if (childNode.nodeType === TEXT_NODE_TYPE) { - i += hightlightTextNode(childNode) ? 1 : 0; - } else { - traverse(childNode, hightlightTextNode); - } - } - } - }; - function getRegex(patterns, caseSensitive, wordsOnly) { - var escapedPatterns = [], regexStr; - for (var i = 0, len = patterns.length; i < len; i++) { - escapedPatterns.push(_.escapeRegExChars(patterns[i])); - } - regexStr = wordsOnly ? "\\b(" + escapedPatterns.join("|") + ")\\b" : "(" + escapedPatterns.join("|") + ")"; - return caseSensitive ? new RegExp(regexStr) : new RegExp(regexStr, "i"); - } - }(window.document); - var Input = function() { - "use strict"; - var specialKeyCodeMap; - specialKeyCodeMap = { - 9: "tab", - 27: "esc", - 37: "left", - 39: "right", - 13: "enter", - 38: "up", - 40: "down" - }; - function Input(o, www) { - o = o || {}; - if (!o.input) { - $.error("input is missing"); - } - www.mixin(this); - this.$hint = $(o.hint); - this.$input = $(o.input); - this.query = this.$input.val(); - this.queryWhenFocused = this.hasFocus() ? this.query : null; - this.$overflowHelper = buildOverflowHelper(this.$input); - this._checkLanguageDirection(); - if (this.$hint.length === 0) { - this.setHint = this.getHint = this.clearHint = this.clearHintIfInvalid = _.noop; - } - } - Input.normalizeQuery = function(str) { - return _.toStr(str).replace(/^\s*/g, "").replace(/\s{2,}/g, " "); - }; - _.mixin(Input.prototype, EventEmitter, { - _onBlur: function onBlur() { - this.resetInputValue(); - this.trigger("blurred"); - }, - _onFocus: function onFocus() { - this.queryWhenFocused = this.query; - this.trigger("focused"); - }, - _onKeydown: function onKeydown($e) { - var keyName = specialKeyCodeMap[$e.which || $e.keyCode]; - this._managePreventDefault(keyName, $e); - if (keyName && this._shouldTrigger(keyName, $e)) { - this.trigger(keyName + "Keyed", $e); - } - }, - _onInput: function onInput() { - this._setQuery(this.getInputValue()); - this.clearHintIfInvalid(); - this._checkLanguageDirection(); - }, - _managePreventDefault: function managePreventDefault(keyName, $e) { - var preventDefault; - switch (keyName) { - case "up": - case "down": - preventDefault = !withModifier($e); - break; - - default: - preventDefault = false; - } - preventDefault && $e.preventDefault(); - }, - _shouldTrigger: function shouldTrigger(keyName, $e) { - var trigger; - switch (keyName) { - case "tab": - trigger = !withModifier($e); - break; - - default: - trigger = true; - } - return trigger; - }, - _checkLanguageDirection: function checkLanguageDirection() { - var dir = (this.$input.css("direction") || "ltr").toLowerCase(); - if (this.dir !== dir) { - this.dir = dir; - this.$hint.attr("dir", dir); - this.trigger("langDirChanged", dir); - } - }, - _setQuery: function setQuery(val, silent) { - var areEquivalent, hasDifferentWhitespace; - areEquivalent = areQueriesEquivalent(val, this.query); - hasDifferentWhitespace = areEquivalent ? this.query.length !== val.length : false; - this.query = val; - if (!silent && !areEquivalent) { - this.trigger("queryChanged", this.query); - } else if (!silent && hasDifferentWhitespace) { - this.trigger("whitespaceChanged", this.query); - } - }, - bind: function() { - var that = this, onBlur, onFocus, onKeydown, onInput; - onBlur = _.bind(this._onBlur, this); - onFocus = _.bind(this._onFocus, this); - onKeydown = _.bind(this._onKeydown, this); - onInput = _.bind(this._onInput, this); - this.$input.on("blur.tt", onBlur).on("focus.tt", onFocus).on("keydown.tt", onKeydown); - if (!_.isMsie() || _.isMsie() > 9) { - this.$input.on("input.tt", onInput); - } else { - this.$input.on("keydown.tt keypress.tt cut.tt paste.tt", function($e) { - if (specialKeyCodeMap[$e.which || $e.keyCode]) { - return; - } - _.defer(_.bind(that._onInput, that, $e)); - }); - } - return this; - }, - focus: function focus() { - this.$input.focus(); - }, - blur: function blur() { - this.$input.blur(); - }, - getLangDir: function getLangDir() { - return this.dir; - }, - getQuery: function getQuery() { - return this.query || ""; - }, - setQuery: function setQuery(val, silent) { - this.setInputValue(val); - this._setQuery(val, silent); - }, - hasQueryChangedSinceLastFocus: function hasQueryChangedSinceLastFocus() { - return this.query !== this.queryWhenFocused; - }, - getInputValue: function getInputValue() { - return this.$input.val(); - }, - setInputValue: function setInputValue(value) { - this.$input.val(value); - this.clearHintIfInvalid(); - this._checkLanguageDirection(); - }, - resetInputValue: function resetInputValue() { - this.setInputValue(this.query); - }, - getHint: function getHint() { - return this.$hint.val(); - }, - setHint: function setHint(value) { - this.$hint.val(value); - }, - clearHint: function clearHint() { - this.setHint(""); - }, - clearHintIfInvalid: function clearHintIfInvalid() { - var val, hint, valIsPrefixOfHint, isValid; - val = this.getInputValue(); - hint = this.getHint(); - valIsPrefixOfHint = val !== hint && hint.indexOf(val) === 0; - isValid = val !== "" && valIsPrefixOfHint && !this.hasOverflow(); - !isValid && this.clearHint(); - }, - hasFocus: function hasFocus() { - return this.$input.is(":focus"); - }, - hasOverflow: function hasOverflow() { - var constraint = this.$input.width() - 2; - this.$overflowHelper.text(this.getInputValue()); - return this.$overflowHelper.width() >= constraint; - }, - isCursorAtEnd: function() { - var valueLength, selectionStart, range; - valueLength = this.$input.val().length; - selectionStart = this.$input[0].selectionStart; - if (_.isNumber(selectionStart)) { - return selectionStart === valueLength; - } else if (document.selection) { - range = document.selection.createRange(); - range.moveStart("character", -valueLength); - return valueLength === range.text.length; - } - return true; - }, - destroy: function destroy() { - this.$hint.off(".tt"); - this.$input.off(".tt"); - this.$overflowHelper.remove(); - this.$hint = this.$input = this.$overflowHelper = $("
    "); - } - }); - return Input; - function buildOverflowHelper($input) { - return $('').css({ - position: "absolute", - visibility: "hidden", - whiteSpace: "pre", - fontFamily: $input.css("font-family"), - fontSize: $input.css("font-size"), - fontStyle: $input.css("font-style"), - fontVariant: $input.css("font-variant"), - fontWeight: $input.css("font-weight"), - wordSpacing: $input.css("word-spacing"), - letterSpacing: $input.css("letter-spacing"), - textIndent: $input.css("text-indent"), - textRendering: $input.css("text-rendering"), - textTransform: $input.css("text-transform") - }).insertAfter($input); - } - function areQueriesEquivalent(a, b) { - return Input.normalizeQuery(a) === Input.normalizeQuery(b); - } - function withModifier($e) { - return $e.altKey || $e.ctrlKey || $e.metaKey || $e.shiftKey; - } - }(); - var Dataset = function() { - "use strict"; - var keys, nameGenerator; - keys = { - val: "tt-selectable-display", - obj: "tt-selectable-object" - }; - nameGenerator = _.getIdGenerator(); - function Dataset(o, www) { - o = o || {}; - o.templates = o.templates || {}; - o.templates.notFound = o.templates.notFound || o.templates.empty; - if (!o.source) { - $.error("missing source"); - } - if (!o.node) { - $.error("missing node"); - } - if (o.name && !isValidName(o.name)) { - $.error("invalid dataset name: " + o.name); - } - www.mixin(this); - this.highlight = !!o.highlight; - this.name = o.name || nameGenerator(); - this.limit = o.limit || 5; - this.displayFn = getDisplayFn(o.display || o.displayKey); - this.templates = getTemplates(o.templates, this.displayFn); - this.source = o.source.__ttAdapter ? o.source.__ttAdapter() : o.source; - this.async = _.isUndefined(o.async) ? this.source.length > 2 : !!o.async; - this._resetLastSuggestion(); - this.$el = $(o.node).addClass(this.classes.dataset).addClass(this.classes.dataset + "-" + this.name); - } - Dataset.extractData = function extractData(el) { - var $el = $(el); - if ($el.data(keys.obj)) { - return { - val: $el.data(keys.val) || "", - obj: $el.data(keys.obj) || null - }; - } - return null; - }; - _.mixin(Dataset.prototype, EventEmitter, { - _overwrite: function overwrite(query, suggestions) { - suggestions = suggestions || []; - if (suggestions.length) { - this._renderSuggestions(query, suggestions); - } else if (this.async && this.templates.pending) { - this._renderPending(query); - } else if (!this.async && this.templates.notFound) { - this._renderNotFound(query); - } else { - this._empty(); - } - this.trigger("rendered", this.name, suggestions, false); - }, - _append: function append(query, suggestions) { - suggestions = suggestions || []; - if (suggestions.length && this.$lastSuggestion.length) { - this._appendSuggestions(query, suggestions); - } else if (suggestions.length) { - this._renderSuggestions(query, suggestions); - } else if (!this.$lastSuggestion.length && this.templates.notFound) { - this._renderNotFound(query); - } - this.trigger("rendered", this.name, suggestions, true); - }, - _renderSuggestions: function renderSuggestions(query, suggestions) { - var $fragment; - $fragment = this._getSuggestionsFragment(query, suggestions); - this.$lastSuggestion = $fragment.children().last(); - this.$el.html($fragment).prepend(this._getHeader(query, suggestions)).append(this._getFooter(query, suggestions)); - }, - _appendSuggestions: function appendSuggestions(query, suggestions) { - var $fragment, $lastSuggestion; - $fragment = this._getSuggestionsFragment(query, suggestions); - $lastSuggestion = $fragment.children().last(); - this.$lastSuggestion.after($fragment); - this.$lastSuggestion = $lastSuggestion; - }, - _renderPending: function renderPending(query) { - var template = this.templates.pending; - this._resetLastSuggestion(); - template && this.$el.html(template({ - query: query, - dataset: this.name - })); - }, - _renderNotFound: function renderNotFound(query) { - var template = this.templates.notFound; - this._resetLastSuggestion(); - template && this.$el.html(template({ - query: query, - dataset: this.name - })); - }, - _empty: function empty() { - this.$el.empty(); - this._resetLastSuggestion(); - }, - _getSuggestionsFragment: function getSuggestionsFragment(query, suggestions) { - var that = this, fragment; - fragment = document.createDocumentFragment(); - _.each(suggestions, function getSuggestionNode(suggestion) { - var $el, context; - context = that._injectQuery(query, suggestion); - $el = $(that.templates.suggestion(context)).data(keys.obj, suggestion).data(keys.val, that.displayFn(suggestion)).addClass(that.classes.suggestion + " " + that.classes.selectable); - fragment.appendChild($el[0]); - }); - this.highlight && highlight({ - className: this.classes.highlight, - node: fragment, - pattern: query - }); - return $(fragment); - }, - _getFooter: function getFooter(query, suggestions) { - return this.templates.footer ? this.templates.footer({ - query: query, - suggestions: suggestions, - dataset: this.name - }) : null; - }, - _getHeader: function getHeader(query, suggestions) { - return this.templates.header ? this.templates.header({ - query: query, - suggestions: suggestions, - dataset: this.name - }) : null; - }, - _resetLastSuggestion: function resetLastSuggestion() { - this.$lastSuggestion = $(); - }, - _injectQuery: function injectQuery(query, obj) { - return _.isObject(obj) ? _.mixin({ - _query: query - }, obj) : obj; - }, - update: function update(query) { - var that = this, canceled = false, syncCalled = false, rendered = 0; - this.cancel(); - this.cancel = function cancel() { - canceled = true; - that.cancel = $.noop; - that.async && that.trigger("asyncCanceled", query); - }; - this.source(query, sync, async); - !syncCalled && sync([]); - function sync(suggestions) { - if (syncCalled) { - return; - } - syncCalled = true; - suggestions = (suggestions || []).slice(0, that.limit); - rendered = suggestions.length; - that._overwrite(query, suggestions); - if (rendered < that.limit && that.async) { - that.trigger("asyncRequested", query); - } - } - function async(suggestions) { - suggestions = suggestions || []; - if (!canceled && rendered < that.limit) { - that.cancel = $.noop; - rendered += suggestions.length; - that._append(query, suggestions.slice(0, that.limit - rendered)); - that.async && that.trigger("asyncReceived", query); - } - } - }, - cancel: $.noop, - clear: function clear() { - this._empty(); - this.cancel(); - this.trigger("cleared"); - }, - isEmpty: function isEmpty() { - return this.$el.is(":empty"); - }, - destroy: function destroy() { - this.$el = $("
    "); - } - }); - return Dataset; - function getDisplayFn(display) { - display = display || _.stringify; - return _.isFunction(display) ? display : displayFn; - function displayFn(obj) { - return obj[display]; - } - } - function getTemplates(templates, displayFn) { - return { - notFound: templates.notFound && _.templatify(templates.notFound), - pending: templates.pending && _.templatify(templates.pending), - header: templates.header && _.templatify(templates.header), - footer: templates.footer && _.templatify(templates.footer), - suggestion: templates.suggestion || suggestionTemplate - }; - function suggestionTemplate(context) { - return $("
    ").text(displayFn(context)); - } - } - function isValidName(str) { - return /^[_a-zA-Z0-9-]+$/.test(str); - } - }(); - var Menu = function() { - "use strict"; - function Menu(o, www) { - var that = this; - o = o || {}; - if (!o.node) { - $.error("node is required"); - } - www.mixin(this); - this.$node = $(o.node); - this.query = null; - this.datasets = _.map(o.datasets, initializeDataset); - function initializeDataset(oDataset) { - var node = that.$node.find(oDataset.node).first(); - oDataset.node = node.length ? node : $("
    ").appendTo(that.$node); - return new Dataset(oDataset, www); - } - } - _.mixin(Menu.prototype, EventEmitter, { - _onSelectableClick: function onSelectableClick($e) { - this.trigger("selectableClicked", $($e.currentTarget)); - }, - _onRendered: function onRendered(type, dataset, suggestions, async) { - this.$node.toggleClass(this.classes.empty, this._allDatasetsEmpty()); - this.trigger("datasetRendered", dataset, suggestions, async); - }, - _onCleared: function onCleared() { - this.$node.toggleClass(this.classes.empty, this._allDatasetsEmpty()); - this.trigger("datasetCleared"); - }, - _propagate: function propagate() { - this.trigger.apply(this, arguments); - }, - _allDatasetsEmpty: function allDatasetsEmpty() { - return _.every(this.datasets, isDatasetEmpty); - function isDatasetEmpty(dataset) { - return dataset.isEmpty(); - } - }, - _getSelectables: function getSelectables() { - return this.$node.find(this.selectors.selectable); - }, - _removeCursor: function _removeCursor() { - var $selectable = this.getActiveSelectable(); - $selectable && $selectable.removeClass(this.classes.cursor); - }, - _ensureVisible: function ensureVisible($el) { - var elTop, elBottom, nodeScrollTop, nodeHeight; - elTop = $el.position().top; - elBottom = elTop + $el.outerHeight(true); - nodeScrollTop = this.$node.scrollTop(); - nodeHeight = this.$node.height() + parseInt(this.$node.css("paddingTop"), 10) + parseInt(this.$node.css("paddingBottom"), 10); - if (elTop < 0) { - this.$node.scrollTop(nodeScrollTop + elTop); - } else if (nodeHeight < elBottom) { - this.$node.scrollTop(nodeScrollTop + (elBottom - nodeHeight)); - } - }, - bind: function() { - var that = this, onSelectableClick; - onSelectableClick = _.bind(this._onSelectableClick, this); - this.$node.on("click.tt", this.selectors.selectable, onSelectableClick); - _.each(this.datasets, function(dataset) { - dataset.onSync("asyncRequested", that._propagate, that).onSync("asyncCanceled", that._propagate, that).onSync("asyncReceived", that._propagate, that).onSync("rendered", that._onRendered, that).onSync("cleared", that._onCleared, that); - }); - return this; - }, - isOpen: function isOpen() { - return this.$node.hasClass(this.classes.open); - }, - open: function open() { - this.$node.addClass(this.classes.open); - }, - close: function close() { - this.$node.removeClass(this.classes.open); - this._removeCursor(); - }, - setLanguageDirection: function setLanguageDirection(dir) { - this.$node.attr("dir", dir); - }, - selectableRelativeToCursor: function selectableRelativeToCursor(delta) { - var $selectables, $oldCursor, oldIndex, newIndex; - $oldCursor = this.getActiveSelectable(); - $selectables = this._getSelectables(); - oldIndex = $oldCursor ? $selectables.index($oldCursor) : -1; - newIndex = oldIndex + delta; - newIndex = (newIndex + 1) % ($selectables.length + 1) - 1; - newIndex = newIndex < -1 ? $selectables.length - 1 : newIndex; - return newIndex === -1 ? null : $selectables.eq(newIndex); - }, - setCursor: function setCursor($selectable) { - this._removeCursor(); - if ($selectable = $selectable && $selectable.first()) { - $selectable.addClass(this.classes.cursor); - this._ensureVisible($selectable); - } - }, - getSelectableData: function getSelectableData($el) { - return $el && $el.length ? Dataset.extractData($el) : null; - }, - getActiveSelectable: function getActiveSelectable() { - var $selectable = this._getSelectables().filter(this.selectors.cursor).first(); - return $selectable.length ? $selectable : null; - }, - getTopSelectable: function getTopSelectable() { - var $selectable = this._getSelectables().first(); - return $selectable.length ? $selectable : null; - }, - update: function update(query) { - var isValidUpdate = query !== this.query; - if (isValidUpdate) { - this.query = query; - _.each(this.datasets, updateDataset); - } - return isValidUpdate; - function updateDataset(dataset) { - dataset.update(query); - } - }, - empty: function empty() { - _.each(this.datasets, clearDataset); - this.query = null; - this.$node.addClass(this.classes.empty); - function clearDataset(dataset) { - dataset.clear(); - } - }, - destroy: function destroy() { - this.$node.off(".tt"); - this.$node = $("
    "); - _.each(this.datasets, destroyDataset); - function destroyDataset(dataset) { - dataset.destroy(); - } - } - }); - return Menu; - }(); - var DefaultMenu = function() { - "use strict"; - var s = Menu.prototype; - function DefaultMenu() { - Menu.apply(this, [].slice.call(arguments, 0)); - } - _.mixin(DefaultMenu.prototype, Menu.prototype, { - open: function open() { - !this._allDatasetsEmpty() && this._show(); - return s.open.apply(this, [].slice.call(arguments, 0)); - }, - close: function close() { - this._hide(); - return s.close.apply(this, [].slice.call(arguments, 0)); - }, - _onRendered: function onRendered() { - if (this._allDatasetsEmpty()) { - this._hide(); - } else { - this.isOpen() && this._show(); - } - return s._onRendered.apply(this, [].slice.call(arguments, 0)); - }, - _onCleared: function onCleared() { - if (this._allDatasetsEmpty()) { - this._hide(); - } else { - this.isOpen() && this._show(); - } - return s._onCleared.apply(this, [].slice.call(arguments, 0)); - }, - setLanguageDirection: function setLanguageDirection(dir) { - this.$node.css(dir === "ltr" ? this.css.ltr : this.css.rtl); - return s.setLanguageDirection.apply(this, [].slice.call(arguments, 0)); - }, - _hide: function hide() { - this.$node.hide(); - }, - _show: function show() { - this.$node.css("display", "block"); - } - }); - return DefaultMenu; - }(); - var Typeahead = function() { - "use strict"; - function Typeahead(o, www) { - var onFocused, onBlurred, onEnterKeyed, onTabKeyed, onEscKeyed, onUpKeyed, onDownKeyed, onLeftKeyed, onRightKeyed, onQueryChanged, onWhitespaceChanged; - o = o || {}; - if (!o.input) { - $.error("missing input"); - } - if (!o.menu) { - $.error("missing menu"); - } - if (!o.eventBus) { - $.error("missing event bus"); - } - www.mixin(this); - this.eventBus = o.eventBus; - this.minLength = _.isNumber(o.minLength) ? o.minLength : 1; - this.input = o.input; - this.menu = o.menu; - this.enabled = true; - this.active = false; - this.input.hasFocus() && this.activate(); - this.dir = this.input.getLangDir(); - this._hacks(); - this.menu.bind().onSync("selectableClicked", this._onSelectableClicked, this).onSync("asyncRequested", this._onAsyncRequested, this).onSync("asyncCanceled", this._onAsyncCanceled, this).onSync("asyncReceived", this._onAsyncReceived, this).onSync("datasetRendered", this._onDatasetRendered, this).onSync("datasetCleared", this._onDatasetCleared, this); - onFocused = c(this, "activate", "open", "_onFocused"); - onBlurred = c(this, "deactivate", "_onBlurred"); - onEnterKeyed = c(this, "isActive", "isOpen", "_onEnterKeyed"); - onTabKeyed = c(this, "isActive", "isOpen", "_onTabKeyed"); - onEscKeyed = c(this, "isActive", "_onEscKeyed"); - onUpKeyed = c(this, "isActive", "open", "_onUpKeyed"); - onDownKeyed = c(this, "isActive", "open", "_onDownKeyed"); - onLeftKeyed = c(this, "isActive", "isOpen", "_onLeftKeyed"); - onRightKeyed = c(this, "isActive", "isOpen", "_onRightKeyed"); - onQueryChanged = c(this, "_openIfActive", "_onQueryChanged"); - onWhitespaceChanged = c(this, "_openIfActive", "_onWhitespaceChanged"); - this.input.bind().onSync("focused", onFocused, this).onSync("blurred", onBlurred, this).onSync("enterKeyed", onEnterKeyed, this).onSync("tabKeyed", onTabKeyed, this).onSync("escKeyed", onEscKeyed, this).onSync("upKeyed", onUpKeyed, this).onSync("downKeyed", onDownKeyed, this).onSync("leftKeyed", onLeftKeyed, this).onSync("rightKeyed", onRightKeyed, this).onSync("queryChanged", onQueryChanged, this).onSync("whitespaceChanged", onWhitespaceChanged, this).onSync("langDirChanged", this._onLangDirChanged, this); - } - _.mixin(Typeahead.prototype, { - _hacks: function hacks() { - var $input, $menu; - $input = this.input.$input || $("
    "); - $menu = this.menu.$node || $("
    "); - $input.on("blur.tt", function($e) { - var active, isActive, hasActive; - active = document.activeElement; - isActive = $menu.is(active); - hasActive = $menu.has(active).length > 0; - if (_.isMsie() && (isActive || hasActive)) { - $e.preventDefault(); - $e.stopImmediatePropagation(); - _.defer(function() { - $input.focus(); - }); - } - }); - $menu.on("mousedown.tt", function($e) { - $e.preventDefault(); - }); - }, - _onSelectableClicked: function onSelectableClicked(type, $el) { - this.select($el); - }, - _onDatasetCleared: function onDatasetCleared() { - this._updateHint(); - }, - _onDatasetRendered: function onDatasetRendered(type, dataset, suggestions, async) { - this._updateHint(); - this.eventBus.trigger("render", suggestions, async, dataset); - }, - _onAsyncRequested: function onAsyncRequested(type, dataset, query) { - this.eventBus.trigger("asyncrequest", query, dataset); - }, - _onAsyncCanceled: function onAsyncCanceled(type, dataset, query) { - this.eventBus.trigger("asynccancel", query, dataset); - }, - _onAsyncReceived: function onAsyncReceived(type, dataset, query) { - this.eventBus.trigger("asyncreceive", query, dataset); - }, - _onFocused: function onFocused() { - this._minLengthMet() && this.menu.update(this.input.getQuery()); - }, - _onBlurred: function onBlurred() { - if (this.input.hasQueryChangedSinceLastFocus()) { - this.eventBus.trigger("change", this.input.getQuery()); - } - }, - _onEnterKeyed: function onEnterKeyed(type, $e) { - var $selectable; - if ($selectable = this.menu.getActiveSelectable()) { - this.select($selectable) && $e.preventDefault(); - } - }, - _onTabKeyed: function onTabKeyed(type, $e) { - var $selectable; - if ($selectable = this.menu.getActiveSelectable()) { - this.select($selectable) && $e.preventDefault(); - } else if ($selectable = this.menu.getTopSelectable()) { - this.autocomplete($selectable) && $e.preventDefault(); - } - }, - _onEscKeyed: function onEscKeyed() { - this.close(); - }, - _onUpKeyed: function onUpKeyed() { - this.moveCursor(-1); - }, - _onDownKeyed: function onDownKeyed() { - this.moveCursor(+1); - }, - _onLeftKeyed: function onLeftKeyed() { - if (this.dir === "rtl" && this.input.isCursorAtEnd()) { - this.autocomplete(this.menu.getTopSelectable()); - } - }, - _onRightKeyed: function onRightKeyed() { - if (this.dir === "ltr" && this.input.isCursorAtEnd()) { - this.autocomplete(this.menu.getTopSelectable()); - } - }, - _onQueryChanged: function onQueryChanged(e, query) { - this._minLengthMet(query) ? this.menu.update(query) : this.menu.empty(); - }, - _onWhitespaceChanged: function onWhitespaceChanged() { - this._updateHint(); - }, - _onLangDirChanged: function onLangDirChanged(e, dir) { - if (this.dir !== dir) { - this.dir = dir; - this.menu.setLanguageDirection(dir); - } - }, - _openIfActive: function openIfActive() { - this.isActive() && this.open(); - }, - _minLengthMet: function minLengthMet(query) { - query = _.isString(query) ? query : this.input.getQuery() || ""; - return query.length >= this.minLength; - }, - _updateHint: function updateHint() { - var $selectable, data, val, query, escapedQuery, frontMatchRegEx, match; - $selectable = this.menu.getTopSelectable(); - data = this.menu.getSelectableData($selectable); - val = this.input.getInputValue(); - if (data && !_.isBlankString(val) && !this.input.hasOverflow()) { - query = Input.normalizeQuery(val); - escapedQuery = _.escapeRegExChars(query); - frontMatchRegEx = new RegExp("^(?:" + escapedQuery + ")(.+$)", "i"); - match = frontMatchRegEx.exec(data.val); - match && this.input.setHint(val + match[1]); - } else { - this.input.clearHint(); - } - }, - isEnabled: function isEnabled() { - return this.enabled; - }, - enable: function enable() { - this.enabled = true; - }, - disable: function disable() { - this.enabled = false; - }, - isActive: function isActive() { - return this.active; - }, - activate: function activate() { - if (this.isActive()) { - return true; - } else if (!this.isEnabled() || this.eventBus.before("active")) { - return false; - } else { - this.active = true; - this.eventBus.trigger("active"); - return true; - } - }, - deactivate: function deactivate() { - if (!this.isActive()) { - return true; - } else if (this.eventBus.before("idle")) { - return false; - } else { - this.active = false; - this.close(); - this.eventBus.trigger("idle"); - return true; - } - }, - isOpen: function isOpen() { - return this.menu.isOpen(); - }, - open: function open() { - if (!this.isOpen() && !this.eventBus.before("open")) { - this.menu.open(); - this._updateHint(); - this.eventBus.trigger("open"); - } - return this.isOpen(); - }, - close: function close() { - if (this.isOpen() && !this.eventBus.before("close")) { - this.menu.close(); - this.input.clearHint(); - this.input.resetInputValue(); - this.eventBus.trigger("close"); - } - return !this.isOpen(); - }, - setVal: function setVal(val) { - this.input.setQuery(_.toStr(val)); - }, - getVal: function getVal() { - return this.input.getQuery(); - }, - select: function select($selectable) { - var data = this.menu.getSelectableData($selectable); - if (data && !this.eventBus.before("select", data.obj)) { - this.input.setQuery(data.val, true); - this.eventBus.trigger("select", data.obj); - this.close(); - return true; - } - return false; - }, - autocomplete: function autocomplete($selectable) { - var query, data, isValid; - query = this.input.getQuery(); - data = this.menu.getSelectableData($selectable); - isValid = data && query !== data.val; - if (isValid && !this.eventBus.before("autocomplete", data.obj)) { - this.input.setQuery(data.val); - this.eventBus.trigger("autocomplete", data.obj); - return true; - } - return false; - }, - moveCursor: function moveCursor(delta) { - var query, $candidate, data, payload, cancelMove; - query = this.input.getQuery(); - $candidate = this.menu.selectableRelativeToCursor(delta); - data = this.menu.getSelectableData($candidate); - payload = data ? data.obj : null; - cancelMove = this._minLengthMet() && this.menu.update(query); - if (!cancelMove && !this.eventBus.before("cursorchange", payload)) { - this.menu.setCursor($candidate); - if (data) { - this.input.setInputValue(data.val); - } else { - this.input.resetInputValue(); - this._updateHint(); - } - this.eventBus.trigger("cursorchange", payload); - return true; - } - return false; - }, - destroy: function destroy() { - this.input.destroy(); - this.menu.destroy(); - } - }); - return Typeahead; - function c(ctx) { - var methods = [].slice.call(arguments, 1); - return function() { - var args = [].slice.call(arguments); - _.each(methods, function(method) { - return ctx[method].apply(ctx, args); - }); - }; - } - }(); - (function() { - "use strict"; - var old, keys, methods; - old = $.fn.typeahead; - keys = { - www: "tt-www", - attrs: "tt-attrs", - typeahead: "tt-typeahead" - }; - methods = { - initialize: function initialize(o, datasets) { - var www; - datasets = _.isArray(datasets) ? datasets : [].slice.call(arguments, 1); - o = o || {}; - www = WWW(o.classNames); - return this.each(attach); - function attach() { - var $input, $wrapper, $hint, $menu, defaultHint, defaultMenu, eventBus, input, menu, typeahead, MenuConstructor; - _.each(datasets, function(d) { - d.highlight = !!o.highlight; - }); - $input = $(this); - $wrapper = $(www.html.wrapper); - $hint = $elOrNull(o.hint); - $menu = $elOrNull(o.menu); - defaultHint = o.hint !== false && !$hint; - defaultMenu = o.menu !== false && !$menu; - defaultHint && ($hint = buildHintFromInput($input, www)); - defaultMenu && ($menu = $(www.html.menu).css(www.css.menu)); - $hint && $hint.val(""); - $input = prepInput($input, www); - if (defaultHint || defaultMenu) { - $wrapper.css(www.css.wrapper); - $input.css(defaultHint ? www.css.input : www.css.inputWithNoHint); - $input.wrap($wrapper).parent().prepend(defaultHint ? $hint : null).append(defaultMenu ? $menu : null); - } - MenuConstructor = defaultMenu ? DefaultMenu : Menu; - eventBus = new EventBus({ - el: $input - }); - input = new Input({ - hint: $hint, - input: $input - }, www); - menu = new MenuConstructor({ - node: $menu, - datasets: datasets - }, www); - typeahead = new Typeahead({ - input: input, - menu: menu, - eventBus: eventBus, - minLength: o.minLength - }, www); - $input.data(keys.www, www); - $input.data(keys.typeahead, typeahead); - } - }, - isEnabled: function isEnabled() { - var enabled; - ttEach(this.first(), function(t) { - enabled = t.isEnabled(); - }); - return enabled; - }, - enable: function enable() { - ttEach(this, function(t) { - t.enable(); - }); - return this; - }, - disable: function disable() { - ttEach(this, function(t) { - t.disable(); - }); - return this; - }, - isActive: function isActive() { - var active; - ttEach(this.first(), function(t) { - active = t.isActive(); - }); - return active; - }, - activate: function activate() { - ttEach(this, function(t) { - t.activate(); - }); - return this; - }, - deactivate: function deactivate() { - ttEach(this, function(t) { - t.deactivate(); - }); - return this; - }, - isOpen: function isOpen() { - var open; - ttEach(this.first(), function(t) { - open = t.isOpen(); - }); - return open; - }, - open: function open() { - ttEach(this, function(t) { - t.open(); - }); - return this; - }, - close: function close() { - ttEach(this, function(t) { - t.close(); - }); - return this; - }, - select: function select(el) { - var success = false, $el = $(el); - ttEach(this.first(), function(t) { - success = t.select($el); - }); - return success; - }, - autocomplete: function autocomplete(el) { - var success = false, $el = $(el); - ttEach(this.first(), function(t) { - success = t.autocomplete($el); - }); - return success; - }, - moveCursor: function moveCursoe(delta) { - var success = false; - ttEach(this.first(), function(t) { - success = t.moveCursor(delta); - }); - return success; - }, - val: function val(newVal) { - var query; - if (!arguments.length) { - ttEach(this.first(), function(t) { - query = t.getVal(); - }); - return query; - } else { - ttEach(this, function(t) { - t.setVal(newVal); - }); - return this; - } - }, - destroy: function destroy() { - ttEach(this, function(typeahead, $input) { - revert($input); - typeahead.destroy(); - }); - return this; - } - }; - $.fn.typeahead = function(method) { - if (methods[method]) { - return methods[method].apply(this, [].slice.call(arguments, 1)); - } else { - return methods.initialize.apply(this, arguments); - } - }; - $.fn.typeahead.noConflict = function noConflict() { - $.fn.typeahead = old; - return this; - }; - function ttEach($els, fn) { - $els.each(function() { - var $input = $(this), typeahead; - (typeahead = $input.data(keys.typeahead)) && fn(typeahead, $input); - }); - } - function buildHintFromInput($input, www) { - return $input.clone().addClass(www.classes.hint).removeData().css(www.css.hint).css(getBackgroundStyles($input)).prop("readonly", true).removeAttr("id name placeholder required").attr({ - autocomplete: "off", - spellcheck: "false", - tabindex: -1 - }); - } - function prepInput($input, www) { - $input.data(keys.attrs, { - dir: $input.attr("dir"), - autocomplete: $input.attr("autocomplete"), - spellcheck: $input.attr("spellcheck"), - style: $input.attr("style") - }); - $input.addClass(www.classes.input).attr({ - autocomplete: "off", - spellcheck: false - }); - try { - !$input.attr("dir") && $input.attr("dir", "auto"); - } catch (e) {} - return $input; - } - function getBackgroundStyles($el) { - return { - backgroundAttachment: $el.css("background-attachment"), - backgroundClip: $el.css("background-clip"), - backgroundColor: $el.css("background-color"), - backgroundImage: $el.css("background-image"), - backgroundOrigin: $el.css("background-origin"), - backgroundPosition: $el.css("background-position"), - backgroundRepeat: $el.css("background-repeat"), - backgroundSize: $el.css("background-size") - }; - } - function revert($input) { - var www, $wrapper; - www = $input.data(keys.www); - $wrapper = $input.parent().filter(www.selectors.wrapper); - _.each($input.data(keys.attrs), function(val, key) { - _.isUndefined(val) ? $input.removeAttr(key) : $input.attr(key, val); - }); - $input.removeData(keys.typeahead).removeData(keys.www).removeData(keys.attr).removeClass(www.classes.input); - if ($wrapper.length) { - $input.detach().insertAfter($wrapper); - $wrapper.remove(); - } - } - function $elOrNull(obj) { - var isValid, $el; - isValid = _.isJQuery(obj) || _.isElement(obj); - $el = isValid ? $(obj).first() : []; - return $el.length ? $el : null; - } - })(); -}); \ No newline at end of file diff --git a/webroot/js/vue-router.js b/webroot/js/vue-router.js new file mode 100644 index 0000000..f42dddd --- /dev/null +++ b/webroot/js/vue-router.js @@ -0,0 +1,3806 @@ +/*! + * vue-router v4.2.2 + * (c) 2023 Eduardo San Martin Morote + * @license MIT + */ +var VueRouter = (function (exports, vue) { + 'use strict'; + + const isBrowser = typeof window !== 'undefined'; + + function isESModule(obj) { + return obj.__esModule || obj[Symbol.toStringTag] === 'Module'; + } + const assign = Object.assign; + function applyToParams(fn, params) { + const newParams = {}; + for (const key in params) { + const value = params[key]; + newParams[key] = isArray(value) + ? value.map(fn) + : fn(value); + } + return newParams; + } + const noop = () => { }; + /** + * Typesafe alternative to Array.isArray + * https://github.com/microsoft/TypeScript/pull/48228 + */ + const isArray = Array.isArray; + + function warn(msg) { + // avoid using ...args as it breaks in older Edge builds + const args = Array.from(arguments).slice(1); + console.warn.apply(console, ['[Vue Router warn]: ' + msg].concat(args)); + } + + const TRAILING_SLASH_RE = /\/$/; + const removeTrailingSlash = (path) => path.replace(TRAILING_SLASH_RE, ''); + /** + * Transforms a URI into a normalized history location + * + * @param parseQuery + * @param location - URI to normalize + * @param currentLocation - current absolute location. Allows resolving relative + * paths. Must start with `/`. Defaults to `/` + * @returns a normalized history location + */ + function parseURL(parseQuery, location, currentLocation = '/') { + let path, query = {}, searchString = '', hash = ''; + // Could use URL and URLSearchParams but IE 11 doesn't support it + // TODO: move to new URL() + const hashPos = location.indexOf('#'); + let searchPos = location.indexOf('?'); + // the hash appears before the search, so it's not part of the search string + if (hashPos < searchPos && hashPos >= 0) { + searchPos = -1; + } + if (searchPos > -1) { + path = location.slice(0, searchPos); + searchString = location.slice(searchPos + 1, hashPos > -1 ? hashPos : location.length); + query = parseQuery(searchString); + } + if (hashPos > -1) { + path = path || location.slice(0, hashPos); + // keep the # character + hash = location.slice(hashPos, location.length); + } + // no search and no query + path = resolveRelativePath(path != null ? path : location, currentLocation); + // empty path means a relative query or hash `?foo=f`, `#thing` + return { + fullPath: path + (searchString && '?') + searchString + hash, + path, + query, + hash, + }; + } + /** + * Stringifies a URL object + * + * @param stringifyQuery + * @param location + */ + function stringifyURL(stringifyQuery, location) { + const query = location.query ? stringifyQuery(location.query) : ''; + return location.path + (query && '?') + query + (location.hash || ''); + } + /** + * Strips off the base from the beginning of a location.pathname in a non-case-sensitive way. + * + * @param pathname - location.pathname + * @param base - base to strip off + */ + function stripBase(pathname, base) { + // no base or base is not found at the beginning + if (!base || !pathname.toLowerCase().startsWith(base.toLowerCase())) + return pathname; + return pathname.slice(base.length) || '/'; + } + /** + * Checks if two RouteLocation are equal. This means that both locations are + * pointing towards the same {@link RouteRecord} and that all `params`, `query` + * parameters and `hash` are the same + * + * @param stringifyQuery - A function that takes a query object of type LocationQueryRaw and returns a string representation of it. + * @param a - first {@link RouteLocation} + * @param b - second {@link RouteLocation} + */ + function isSameRouteLocation(stringifyQuery, a, b) { + const aLastIndex = a.matched.length - 1; + const bLastIndex = b.matched.length - 1; + return (aLastIndex > -1 && + aLastIndex === bLastIndex && + isSameRouteRecord(a.matched[aLastIndex], b.matched[bLastIndex]) && + isSameRouteLocationParams(a.params, b.params) && + stringifyQuery(a.query) === stringifyQuery(b.query) && + a.hash === b.hash); + } + /** + * Check if two `RouteRecords` are equal. Takes into account aliases: they are + * considered equal to the `RouteRecord` they are aliasing. + * + * @param a - first {@link RouteRecord} + * @param b - second {@link RouteRecord} + */ + function isSameRouteRecord(a, b) { + // since the original record has an undefined value for aliasOf + // but all aliases point to the original record, this will always compare + // the original record + return (a.aliasOf || a) === (b.aliasOf || b); + } + function isSameRouteLocationParams(a, b) { + if (Object.keys(a).length !== Object.keys(b).length) + return false; + for (const key in a) { + if (!isSameRouteLocationParamsValue(a[key], b[key])) + return false; + } + return true; + } + function isSameRouteLocationParamsValue(a, b) { + return isArray(a) + ? isEquivalentArray(a, b) + : isArray(b) + ? isEquivalentArray(b, a) + : a === b; + } + /** + * Check if two arrays are the same or if an array with one single entry is the + * same as another primitive value. Used to check query and parameters + * + * @param a - array of values + * @param b - array of values or a single value + */ + function isEquivalentArray(a, b) { + return isArray(b) + ? a.length === b.length && a.every((value, i) => value === b[i]) + : a.length === 1 && a[0] === b; + } + /** + * Resolves a relative path that starts with `.`. + * + * @param to - path location we are resolving + * @param from - currentLocation.path, should start with `/` + */ + function resolveRelativePath(to, from) { + if (to.startsWith('/')) + return to; + if (!from.startsWith('/')) { + warn(`Cannot resolve a relative location without an absolute path. Trying to resolve "${to}" from "${from}". It should look like "/${from}".`); + return to; + } + if (!to) + return from; + const fromSegments = from.split('/'); + const toSegments = to.split('/'); + const lastToSegment = toSegments[toSegments.length - 1]; + // make . and ./ the same (../ === .., ../../ === ../..) + // this is the same behavior as new URL() + if (lastToSegment === '..' || lastToSegment === '.') { + toSegments.push(''); + } + let position = fromSegments.length - 1; + let toPosition; + let segment; + for (toPosition = 0; toPosition < toSegments.length; toPosition++) { + segment = toSegments[toPosition]; + // we stay on the same position + if (segment === '.') + continue; + // go up in the from array + if (segment === '..') { + // we can't go below zero, but we still need to increment toPosition + if (position > 1) + position--; + // continue + } + // we reached a non-relative path, we stop here + else + break; + } + return (fromSegments.slice(0, position).join('/') + + '/' + + toSegments + // ensure we use at least the last element in the toSegments + .slice(toPosition - (toPosition === toSegments.length ? 1 : 0)) + .join('/')); + } + + var NavigationType; + (function (NavigationType) { + NavigationType["pop"] = "pop"; + NavigationType["push"] = "push"; + })(NavigationType || (NavigationType = {})); + var NavigationDirection; + (function (NavigationDirection) { + NavigationDirection["back"] = "back"; + NavigationDirection["forward"] = "forward"; + NavigationDirection["unknown"] = ""; + })(NavigationDirection || (NavigationDirection = {})); + /** + * Starting location for Histories + */ + const START = ''; + // Generic utils + /** + * Normalizes a base by removing any trailing slash and reading the base tag if + * present. + * + * @param base - base to normalize + */ + function normalizeBase(base) { + if (!base) { + if (isBrowser) { + // respect tag + const baseEl = document.querySelector('base'); + base = (baseEl && baseEl.getAttribute('href')) || '/'; + // strip full URL origin + base = base.replace(/^\w+:\/\/[^\/]+/, ''); + } + else { + base = '/'; + } + } + // ensure leading slash when it was removed by the regex above avoid leading + // slash with hash because the file could be read from the disk like file:// + // and the leading slash would cause problems + if (base[0] !== '/' && base[0] !== '#') + base = '/' + base; + // remove the trailing slash so all other method can just do `base + fullPath` + // to build an href + return removeTrailingSlash(base); + } + // remove any character before the hash + const BEFORE_HASH_RE = /^[^#]+#/; + function createHref(base, location) { + return base.replace(BEFORE_HASH_RE, '#') + location; + } + + function getElementPosition(el, offset) { + const docRect = document.documentElement.getBoundingClientRect(); + const elRect = el.getBoundingClientRect(); + return { + behavior: offset.behavior, + left: elRect.left - docRect.left - (offset.left || 0), + top: elRect.top - docRect.top - (offset.top || 0), + }; + } + const computeScrollPosition = () => ({ + left: window.pageXOffset, + top: window.pageYOffset, + }); + function scrollToPosition(position) { + let scrollToOptions; + if ('el' in position) { + const positionEl = position.el; + const isIdSelector = typeof positionEl === 'string' && positionEl.startsWith('#'); + /** + * `id`s can accept pretty much any characters, including CSS combinators + * like `>` or `~`. It's still possible to retrieve elements using + * `document.getElementById('~')` but it needs to be escaped when using + * `document.querySelector('#\\~')` for it to be valid. The only + * requirements for `id`s are them to be unique on the page and to not be + * empty (`id=""`). Because of that, when passing an id selector, it should + * be properly escaped for it to work with `querySelector`. We could check + * for the id selector to be simple (no CSS combinators `+ >~`) but that + * would make things inconsistent since they are valid characters for an + * `id` but would need to be escaped when using `querySelector`, breaking + * their usage and ending up in no selector returned. Selectors need to be + * escaped: + * + * - `#1-thing` becomes `#\31 -thing` + * - `#with~symbols` becomes `#with\\~symbols` + * + * - More information about the topic can be found at + * https://mathiasbynens.be/notes/html5-id-class. + * - Practical example: https://mathiasbynens.be/demo/html5-id + */ + if (typeof position.el === 'string') { + if (!isIdSelector || !document.getElementById(position.el.slice(1))) { + try { + const foundEl = document.querySelector(position.el); + if (isIdSelector && foundEl) { + warn(`The selector "${position.el}" should be passed as "el: document.querySelector('${position.el}')" because it starts with "#".`); + // return to avoid other warnings + return; + } + } + catch (err) { + warn(`The selector "${position.el}" is invalid. If you are using an id selector, make sure to escape it. You can find more information about escaping characters in selectors at https://mathiasbynens.be/notes/css-escapes or use CSS.escape (https://developer.mozilla.org/en-US/docs/Web/API/CSS/escape).`); + // return to avoid other warnings + return; + } + } + } + const el = typeof positionEl === 'string' + ? isIdSelector + ? document.getElementById(positionEl.slice(1)) + : document.querySelector(positionEl) + : positionEl; + if (!el) { + warn(`Couldn't find element using selector "${position.el}" returned by scrollBehavior.`); + return; + } + scrollToOptions = getElementPosition(el, position); + } + else { + scrollToOptions = position; + } + if ('scrollBehavior' in document.documentElement.style) + window.scrollTo(scrollToOptions); + else { + window.scrollTo(scrollToOptions.left != null ? scrollToOptions.left : window.pageXOffset, scrollToOptions.top != null ? scrollToOptions.top : window.pageYOffset); + } + } + function getScrollKey(path, delta) { + const position = history.state ? history.state.position - delta : -1; + return position + path; + } + const scrollPositions = new Map(); + function saveScrollPosition(key, scrollPosition) { + scrollPositions.set(key, scrollPosition); + } + function getSavedScrollPosition(key) { + const scroll = scrollPositions.get(key); + // consume it so it's not used again + scrollPositions.delete(key); + return scroll; + } + // TODO: RFC about how to save scroll position + /** + * ScrollBehavior instance used by the router to compute and restore the scroll + * position when navigating. + */ + // export interface ScrollHandler { + // // returns a scroll position that can be saved in history + // compute(): ScrollPositionEntry + // // can take an extended ScrollPositionEntry + // scroll(position: ScrollPosition): void + // } + // export const scrollHandler: ScrollHandler = { + // compute: computeScroll, + // scroll: scrollToPosition, + // } + + let createBaseLocation = () => location.protocol + '//' + location.host; + /** + * Creates a normalized history location from a window.location object + * @param base - The base path + * @param location - The window.location object + */ + function createCurrentLocation(base, location) { + const { pathname, search, hash } = location; + // allows hash bases like #, /#, #/, #!, #!/, /#!/, or even /folder#end + const hashPos = base.indexOf('#'); + if (hashPos > -1) { + let slicePos = hash.includes(base.slice(hashPos)) + ? base.slice(hashPos).length + : 1; + let pathFromHash = hash.slice(slicePos); + // prepend the starting slash to hash so the url starts with /# + if (pathFromHash[0] !== '/') + pathFromHash = '/' + pathFromHash; + return stripBase(pathFromHash, ''); + } + const path = stripBase(pathname, base); + return path + search + hash; + } + function useHistoryListeners(base, historyState, currentLocation, replace) { + let listeners = []; + let teardowns = []; + // TODO: should it be a stack? a Dict. Check if the popstate listener + // can trigger twice + let pauseState = null; + const popStateHandler = ({ state, }) => { + const to = createCurrentLocation(base, location); + const from = currentLocation.value; + const fromState = historyState.value; + let delta = 0; + if (state) { + currentLocation.value = to; + historyState.value = state; + // ignore the popstate and reset the pauseState + if (pauseState && pauseState === from) { + pauseState = null; + return; + } + delta = fromState ? state.position - fromState.position : 0; + } + else { + replace(to); + } + // console.log({ deltaFromCurrent }) + // Here we could also revert the navigation by calling history.go(-delta) + // this listener will have to be adapted to not trigger again and to wait for the url + // to be updated before triggering the listeners. Some kind of validation function would also + // need to be passed to the listeners so the navigation can be accepted + // call all listeners + listeners.forEach(listener => { + listener(currentLocation.value, from, { + delta, + type: NavigationType.pop, + direction: delta + ? delta > 0 + ? NavigationDirection.forward + : NavigationDirection.back + : NavigationDirection.unknown, + }); + }); + }; + function pauseListeners() { + pauseState = currentLocation.value; + } + function listen(callback) { + // set up the listener and prepare teardown callbacks + listeners.push(callback); + const teardown = () => { + const index = listeners.indexOf(callback); + if (index > -1) + listeners.splice(index, 1); + }; + teardowns.push(teardown); + return teardown; + } + function beforeUnloadListener() { + const { history } = window; + if (!history.state) + return; + history.replaceState(assign({}, history.state, { scroll: computeScrollPosition() }), ''); + } + function destroy() { + for (const teardown of teardowns) + teardown(); + teardowns = []; + window.removeEventListener('popstate', popStateHandler); + window.removeEventListener('beforeunload', beforeUnloadListener); + } + // set up the listeners and prepare teardown callbacks + window.addEventListener('popstate', popStateHandler); + // TODO: could we use 'pagehide' or 'visibilitychange' instead? + // https://developer.chrome.com/blog/page-lifecycle-api/ + window.addEventListener('beforeunload', beforeUnloadListener, { + passive: true, + }); + return { + pauseListeners, + listen, + destroy, + }; + } + /** + * Creates a state object + */ + function buildState(back, current, forward, replaced = false, computeScroll = false) { + return { + back, + current, + forward, + replaced, + position: window.history.length, + scroll: computeScroll ? computeScrollPosition() : null, + }; + } + function useHistoryStateNavigation(base) { + const { history, location } = window; + // private variables + const currentLocation = { + value: createCurrentLocation(base, location), + }; + const historyState = { value: history.state }; + // build current history entry as this is a fresh navigation + if (!historyState.value) { + changeLocation(currentLocation.value, { + back: null, + current: currentLocation.value, + forward: null, + // the length is off by one, we need to decrease it + position: history.length - 1, + replaced: true, + // don't add a scroll as the user may have an anchor, and we want + // scrollBehavior to be triggered without a saved position + scroll: null, + }, true); + } + function changeLocation(to, state, replace) { + /** + * if a base tag is provided, and we are on a normal domain, we have to + * respect the provided `base` attribute because pushState() will use it and + * potentially erase anything before the `#` like at + * https://github.com/vuejs/router/issues/685 where a base of + * `/folder/#` but a base of `/` would erase the `/folder/` section. If + * there is no host, the `` tag makes no sense and if there isn't a + * base tag we can just use everything after the `#`. + */ + const hashIndex = base.indexOf('#'); + const url = hashIndex > -1 + ? (location.host && document.querySelector('base') + ? base + : base.slice(hashIndex)) + to + : createBaseLocation() + base + to; + try { + // BROWSER QUIRK + // NOTE: Safari throws a SecurityError when calling this function 100 times in 30 seconds + history[replace ? 'replaceState' : 'pushState'](state, '', url); + historyState.value = state; + } + catch (err) { + { + warn('Error with push/replace State', err); + } + // Force the navigation, this also resets the call count + location[replace ? 'replace' : 'assign'](url); + } + } + function replace(to, data) { + const state = assign({}, history.state, buildState(historyState.value.back, + // keep back and forward entries but override current position + to, historyState.value.forward, true), data, { position: historyState.value.position }); + changeLocation(to, state, true); + currentLocation.value = to; + } + function push(to, data) { + // Add to current entry the information of where we are going + // as well as saving the current position + const currentState = assign({}, + // use current history state to gracefully handle a wrong call to + // history.replaceState + // https://github.com/vuejs/router/issues/366 + historyState.value, history.state, { + forward: to, + scroll: computeScrollPosition(), + }); + if (!history.state) { + warn(`history.state seems to have been manually replaced without preserving the necessary values. Make sure to preserve existing history state if you are manually calling history.replaceState:\n\n` + + `history.replaceState(history.state, '', url)\n\n` + + `You can find more information at https://next.router.vuejs.org/guide/migration/#usage-of-history-state.`); + } + changeLocation(currentState.current, currentState, true); + const state = assign({}, buildState(currentLocation.value, to, null), { position: currentState.position + 1 }, data); + changeLocation(to, state, false); + currentLocation.value = to; + } + return { + location: currentLocation, + state: historyState, + push, + replace, + }; + } + /** + * Creates an HTML5 history. Most common history for single page applications. + * + * @param base - + */ + function createWebHistory(base) { + base = normalizeBase(base); + const historyNavigation = useHistoryStateNavigation(base); + const historyListeners = useHistoryListeners(base, historyNavigation.state, historyNavigation.location, historyNavigation.replace); + function go(delta, triggerListeners = true) { + if (!triggerListeners) + historyListeners.pauseListeners(); + history.go(delta); + } + const routerHistory = assign({ + // it's overridden right after + location: '', + base, + go, + createHref: createHref.bind(null, base), + }, historyNavigation, historyListeners); + Object.defineProperty(routerHistory, 'location', { + enumerable: true, + get: () => historyNavigation.location.value, + }); + Object.defineProperty(routerHistory, 'state', { + enumerable: true, + get: () => historyNavigation.state.value, + }); + return routerHistory; + } + + /** + * Creates an in-memory based history. The main purpose of this history is to handle SSR. It starts in a special location that is nowhere. + * It's up to the user to replace that location with the starter location by either calling `router.push` or `router.replace`. + * + * @param base - Base applied to all urls, defaults to '/' + * @returns a history object that can be passed to the router constructor + */ + function createMemoryHistory(base = '') { + let listeners = []; + let queue = [START]; + let position = 0; + base = normalizeBase(base); + function setLocation(location) { + position++; + if (position === queue.length) { + // we are at the end, we can simply append a new entry + queue.push(location); + } + else { + // we are in the middle, we remove everything from here in the queue + queue.splice(position); + queue.push(location); + } + } + function triggerListeners(to, from, { direction, delta }) { + const info = { + direction, + delta, + type: NavigationType.pop, + }; + for (const callback of listeners) { + callback(to, from, info); + } + } + const routerHistory = { + // rewritten by Object.defineProperty + location: START, + // TODO: should be kept in queue + state: {}, + base, + createHref: createHref.bind(null, base), + replace(to) { + // remove current entry and decrement position + queue.splice(position--, 1); + setLocation(to); + }, + push(to, data) { + setLocation(to); + }, + listen(callback) { + listeners.push(callback); + return () => { + const index = listeners.indexOf(callback); + if (index > -1) + listeners.splice(index, 1); + }; + }, + destroy() { + listeners = []; + queue = [START]; + position = 0; + }, + go(delta, shouldTrigger = true) { + const from = this.location; + const direction = + // we are considering delta === 0 going forward, but in abstract mode + // using 0 for the delta doesn't make sense like it does in html5 where + // it reloads the page + delta < 0 ? NavigationDirection.back : NavigationDirection.forward; + position = Math.max(0, Math.min(position + delta, queue.length - 1)); + if (shouldTrigger) { + triggerListeners(this.location, from, { + direction, + delta, + }); + } + }, + }; + Object.defineProperty(routerHistory, 'location', { + enumerable: true, + get: () => queue[position], + }); + return routerHistory; + } + + /** + * Creates a hash history. Useful for web applications with no host (e.g. `file://`) or when configuring a server to + * handle any URL is not possible. + * + * @param base - optional base to provide. Defaults to `location.pathname + location.search` If there is a `` tag + * in the `head`, its value will be ignored in favor of this parameter **but note it affects all the history.pushState() + * calls**, meaning that if you use a `` tag, it's `href` value **has to match this parameter** (ignoring anything + * after the `#`). + * + * @example + * ```js + * // at https://example.com/folder + * createWebHashHistory() // gives a url of `https://example.com/folder#` + * createWebHashHistory('/folder/') // gives a url of `https://example.com/folder/#` + * // if the `#` is provided in the base, it won't be added by `createWebHashHistory` + * createWebHashHistory('/folder/#/app/') // gives a url of `https://example.com/folder/#/app/` + * // you should avoid doing this because it changes the original url and breaks copying urls + * createWebHashHistory('/other-folder/') // gives a url of `https://example.com/other-folder/#` + * + * // at file:///usr/etc/folder/index.html + * // for locations with no `host`, the base is ignored + * createWebHashHistory('/iAmIgnored') // gives a url of `file:///usr/etc/folder/index.html#` + * ``` + */ + function createWebHashHistory(base) { + // Make sure this implementation is fine in terms of encoding, specially for IE11 + // for `file://`, directly use the pathname and ignore the base + // location.pathname contains an initial `/` even at the root: `https://example.com` + base = location.host ? base || location.pathname + location.search : ''; + // allow the user to provide a `#` in the middle: `/base/#/app` + if (!base.includes('#')) + base += '#'; + if (!base.endsWith('#/') && !base.endsWith('#')) { + warn(`A hash base must end with a "#":\n"${base}" should be "${base.replace(/#.*$/, '#')}".`); + } + return createWebHistory(base); + } + + function isRouteLocation(route) { + return typeof route === 'string' || (route && typeof route === 'object'); + } + function isRouteName(name) { + return typeof name === 'string' || typeof name === 'symbol'; + } + + /** + * Initial route location where the router is. Can be used in navigation guards + * to differentiate the initial navigation. + * + * @example + * ```js + * import { START_LOCATION } from 'vue-router' + * + * router.beforeEach((to, from) => { + * if (from === START_LOCATION) { + * // initial navigation + * } + * }) + * ``` + */ + const START_LOCATION_NORMALIZED = { + path: '/', + name: undefined, + params: {}, + query: {}, + hash: '', + fullPath: '/', + matched: [], + meta: {}, + redirectedFrom: undefined, + }; + + const NavigationFailureSymbol = Symbol('navigation failure' ); + /** + * Enumeration with all possible types for navigation failures. Can be passed to + * {@link isNavigationFailure} to check for specific failures. + */ + exports.NavigationFailureType = void 0; + (function (NavigationFailureType) { + /** + * An aborted navigation is a navigation that failed because a navigation + * guard returned `false` or called `next(false)` + */ + NavigationFailureType[NavigationFailureType["aborted"] = 4] = "aborted"; + /** + * A cancelled navigation is a navigation that failed because a more recent + * navigation finished started (not necessarily finished). + */ + NavigationFailureType[NavigationFailureType["cancelled"] = 8] = "cancelled"; + /** + * A duplicated navigation is a navigation that failed because it was + * initiated while already being at the exact same location. + */ + NavigationFailureType[NavigationFailureType["duplicated"] = 16] = "duplicated"; + })(exports.NavigationFailureType || (exports.NavigationFailureType = {})); + // DEV only debug messages + const ErrorTypeMessages = { + [1 /* ErrorTypes.MATCHER_NOT_FOUND */]({ location, currentLocation }) { + return `No match for\n ${JSON.stringify(location)}${currentLocation + ? '\nwhile being at\n' + JSON.stringify(currentLocation) + : ''}`; + }, + [2 /* ErrorTypes.NAVIGATION_GUARD_REDIRECT */]({ from, to, }) { + return `Redirected from "${from.fullPath}" to "${stringifyRoute(to)}" via a navigation guard.`; + }, + [4 /* ErrorTypes.NAVIGATION_ABORTED */]({ from, to }) { + return `Navigation aborted from "${from.fullPath}" to "${to.fullPath}" via a navigation guard.`; + }, + [8 /* ErrorTypes.NAVIGATION_CANCELLED */]({ from, to }) { + return `Navigation cancelled from "${from.fullPath}" to "${to.fullPath}" with a new navigation.`; + }, + [16 /* ErrorTypes.NAVIGATION_DUPLICATED */]({ from, to }) { + return `Avoided redundant navigation to current location: "${from.fullPath}".`; + }, + }; + function createRouterError(type, params) { + // keep full error messages in cjs versions + { + return assign(new Error(ErrorTypeMessages[type](params)), { + type, + [NavigationFailureSymbol]: true, + }, params); + } + } + function isNavigationFailure(error, type) { + return (error instanceof Error && + NavigationFailureSymbol in error && + (type == null || !!(error.type & type))); + } + const propertiesToLog = ['params', 'query', 'hash']; + function stringifyRoute(to) { + if (typeof to === 'string') + return to; + if ('path' in to) + return to.path; + const location = {}; + for (const key of propertiesToLog) { + if (key in to) + location[key] = to[key]; + } + return JSON.stringify(location, null, 2); + } + + // default pattern for a param: non-greedy everything but / + const BASE_PARAM_PATTERN = '[^/]+?'; + const BASE_PATH_PARSER_OPTIONS = { + sensitive: false, + strict: false, + start: true, + end: true, + }; + // Special Regex characters that must be escaped in static tokens + const REGEX_CHARS_RE = /[.+*?^${}()[\]/\\]/g; + /** + * Creates a path parser from an array of Segments (a segment is an array of Tokens) + * + * @param segments - array of segments returned by tokenizePath + * @param extraOptions - optional options for the regexp + * @returns a PathParser + */ + function tokensToParser(segments, extraOptions) { + const options = assign({}, BASE_PATH_PARSER_OPTIONS, extraOptions); + // the amount of scores is the same as the length of segments except for the root segment "/" + const score = []; + // the regexp as a string + let pattern = options.start ? '^' : ''; + // extracted keys + const keys = []; + for (const segment of segments) { + // the root segment needs special treatment + const segmentScores = segment.length ? [] : [90 /* PathScore.Root */]; + // allow trailing slash + if (options.strict && !segment.length) + pattern += '/'; + for (let tokenIndex = 0; tokenIndex < segment.length; tokenIndex++) { + const token = segment[tokenIndex]; + // resets the score if we are inside a sub-segment /:a-other-:b + let subSegmentScore = 40 /* PathScore.Segment */ + + (options.sensitive ? 0.25 /* PathScore.BonusCaseSensitive */ : 0); + if (token.type === 0 /* TokenType.Static */) { + // prepend the slash if we are starting a new segment + if (!tokenIndex) + pattern += '/'; + pattern += token.value.replace(REGEX_CHARS_RE, '\\$&'); + subSegmentScore += 40 /* PathScore.Static */; + } + else if (token.type === 1 /* TokenType.Param */) { + const { value, repeatable, optional, regexp } = token; + keys.push({ + name: value, + repeatable, + optional, + }); + const re = regexp ? regexp : BASE_PARAM_PATTERN; + // the user provided a custom regexp /:id(\\d+) + if (re !== BASE_PARAM_PATTERN) { + subSegmentScore += 10 /* PathScore.BonusCustomRegExp */; + // make sure the regexp is valid before using it + try { + new RegExp(`(${re})`); + } + catch (err) { + throw new Error(`Invalid custom RegExp for param "${value}" (${re}): ` + + err.message); + } + } + // when we repeat we must take care of the repeating leading slash + let subPattern = repeatable ? `((?:${re})(?:/(?:${re}))*)` : `(${re})`; + // prepend the slash if we are starting a new segment + if (!tokenIndex) + subPattern = + // avoid an optional / if there are more segments e.g. /:p?-static + // or /:p?-:p2 + optional && segment.length < 2 + ? `(?:/${subPattern})` + : '/' + subPattern; + if (optional) + subPattern += '?'; + pattern += subPattern; + subSegmentScore += 20 /* PathScore.Dynamic */; + if (optional) + subSegmentScore += -8 /* PathScore.BonusOptional */; + if (repeatable) + subSegmentScore += -20 /* PathScore.BonusRepeatable */; + if (re === '.*') + subSegmentScore += -50 /* PathScore.BonusWildcard */; + } + segmentScores.push(subSegmentScore); + } + // an empty array like /home/ -> [[{home}], []] + // if (!segment.length) pattern += '/' + score.push(segmentScores); + } + // only apply the strict bonus to the last score + if (options.strict && options.end) { + const i = score.length - 1; + score[i][score[i].length - 1] += 0.7000000000000001 /* PathScore.BonusStrict */; + } + // TODO: dev only warn double trailing slash + if (!options.strict) + pattern += '/?'; + if (options.end) + pattern += '$'; + // allow paths like /dynamic to only match dynamic or dynamic/... but not dynamic_something_else + else if (options.strict) + pattern += '(?:/|$)'; + const re = new RegExp(pattern, options.sensitive ? '' : 'i'); + function parse(path) { + const match = path.match(re); + const params = {}; + if (!match) + return null; + for (let i = 1; i < match.length; i++) { + const value = match[i] || ''; + const key = keys[i - 1]; + params[key.name] = value && key.repeatable ? value.split('/') : value; + } + return params; + } + function stringify(params) { + let path = ''; + // for optional parameters to allow to be empty + let avoidDuplicatedSlash = false; + for (const segment of segments) { + if (!avoidDuplicatedSlash || !path.endsWith('/')) + path += '/'; + avoidDuplicatedSlash = false; + for (const token of segment) { + if (token.type === 0 /* TokenType.Static */) { + path += token.value; + } + else if (token.type === 1 /* TokenType.Param */) { + const { value, repeatable, optional } = token; + const param = value in params ? params[value] : ''; + if (isArray(param) && !repeatable) { + throw new Error(`Provided param "${value}" is an array but it is not repeatable (* or + modifiers)`); + } + const text = isArray(param) + ? param.join('/') + : param; + if (!text) { + if (optional) { + // if we have more than one optional param like /:a?-static we don't need to care about the optional param + if (segment.length < 2) { + // remove the last slash as we could be at the end + if (path.endsWith('/')) + path = path.slice(0, -1); + // do not append a slash on the next iteration + else + avoidDuplicatedSlash = true; + } + } + else + throw new Error(`Missing required param "${value}"`); + } + path += text; + } + } + } + // avoid empty path when we have multiple optional params + return path || '/'; + } + return { + re, + score, + keys, + parse, + stringify, + }; + } + /** + * Compares an array of numbers as used in PathParser.score and returns a + * number. This function can be used to `sort` an array + * + * @param a - first array of numbers + * @param b - second array of numbers + * @returns 0 if both are equal, < 0 if a should be sorted first, > 0 if b + * should be sorted first + */ + function compareScoreArray(a, b) { + let i = 0; + while (i < a.length && i < b.length) { + const diff = b[i] - a[i]; + // only keep going if diff === 0 + if (diff) + return diff; + i++; + } + // if the last subsegment was Static, the shorter segments should be sorted first + // otherwise sort the longest segment first + if (a.length < b.length) { + return a.length === 1 && a[0] === 40 /* PathScore.Static */ + 40 /* PathScore.Segment */ + ? -1 + : 1; + } + else if (a.length > b.length) { + return b.length === 1 && b[0] === 40 /* PathScore.Static */ + 40 /* PathScore.Segment */ + ? 1 + : -1; + } + return 0; + } + /** + * Compare function that can be used with `sort` to sort an array of PathParser + * + * @param a - first PathParser + * @param b - second PathParser + * @returns 0 if both are equal, < 0 if a should be sorted first, > 0 if b + */ + function comparePathParserScore(a, b) { + let i = 0; + const aScore = a.score; + const bScore = b.score; + while (i < aScore.length && i < bScore.length) { + const comp = compareScoreArray(aScore[i], bScore[i]); + // do not return if both are equal + if (comp) + return comp; + i++; + } + if (Math.abs(bScore.length - aScore.length) === 1) { + if (isLastScoreNegative(aScore)) + return 1; + if (isLastScoreNegative(bScore)) + return -1; + } + // if a and b share the same score entries but b has more, sort b first + return bScore.length - aScore.length; + // this is the ternary version + // return aScore.length < bScore.length + // ? 1 + // : aScore.length > bScore.length + // ? -1 + // : 0 + } + /** + * This allows detecting splats at the end of a path: /home/:id(.*)* + * + * @param score - score to check + * @returns true if the last entry is negative + */ + function isLastScoreNegative(score) { + const last = score[score.length - 1]; + return score.length > 0 && last[last.length - 1] < 0; + } + + const ROOT_TOKEN = { + type: 0 /* TokenType.Static */, + value: '', + }; + const VALID_PARAM_RE = /[a-zA-Z0-9_]/; + // After some profiling, the cache seems to be unnecessary because tokenizePath + // (the slowest part of adding a route) is very fast + // const tokenCache = new Map() + function tokenizePath(path) { + if (!path) + return [[]]; + if (path === '/') + return [[ROOT_TOKEN]]; + if (!path.startsWith('/')) { + throw new Error(`Route paths should start with a "/": "${path}" should be "/${path}".` + ); + } + // if (tokenCache.has(path)) return tokenCache.get(path)! + function crash(message) { + throw new Error(`ERR (${state})/"${buffer}": ${message}`); + } + let state = 0 /* TokenizerState.Static */; + let previousState = state; + const tokens = []; + // the segment will always be valid because we get into the initial state + // with the leading / + let segment; + function finalizeSegment() { + if (segment) + tokens.push(segment); + segment = []; + } + // index on the path + let i = 0; + // char at index + let char; + // buffer of the value read + let buffer = ''; + // custom regexp for a param + let customRe = ''; + function consumeBuffer() { + if (!buffer) + return; + if (state === 0 /* TokenizerState.Static */) { + segment.push({ + type: 0 /* TokenType.Static */, + value: buffer, + }); + } + else if (state === 1 /* TokenizerState.Param */ || + state === 2 /* TokenizerState.ParamRegExp */ || + state === 3 /* TokenizerState.ParamRegExpEnd */) { + if (segment.length > 1 && (char === '*' || char === '+')) + crash(`A repeatable param (${buffer}) must be alone in its segment. eg: '/:ids+.`); + segment.push({ + type: 1 /* TokenType.Param */, + value: buffer, + regexp: customRe, + repeatable: char === '*' || char === '+', + optional: char === '*' || char === '?', + }); + } + else { + crash('Invalid state to consume buffer'); + } + buffer = ''; + } + function addCharToBuffer() { + buffer += char; + } + while (i < path.length) { + char = path[i++]; + if (char === '\\' && state !== 2 /* TokenizerState.ParamRegExp */) { + previousState = state; + state = 4 /* TokenizerState.EscapeNext */; + continue; + } + switch (state) { + case 0 /* TokenizerState.Static */: + if (char === '/') { + if (buffer) { + consumeBuffer(); + } + finalizeSegment(); + } + else if (char === ':') { + consumeBuffer(); + state = 1 /* TokenizerState.Param */; + } + else { + addCharToBuffer(); + } + break; + case 4 /* TokenizerState.EscapeNext */: + addCharToBuffer(); + state = previousState; + break; + case 1 /* TokenizerState.Param */: + if (char === '(') { + state = 2 /* TokenizerState.ParamRegExp */; + } + else if (VALID_PARAM_RE.test(char)) { + addCharToBuffer(); + } + else { + consumeBuffer(); + state = 0 /* TokenizerState.Static */; + // go back one character if we were not modifying + if (char !== '*' && char !== '?' && char !== '+') + i--; + } + break; + case 2 /* TokenizerState.ParamRegExp */: + // TODO: is it worth handling nested regexp? like :p(?:prefix_([^/]+)_suffix) + // it already works by escaping the closing ) + // https://paths.esm.dev/?p=AAMeJbiAwQEcDKbAoAAkP60PG2R6QAvgNaA6AFACM2ABuQBB# + // is this really something people need since you can also write + // /prefix_:p()_suffix + if (char === ')') { + // handle the escaped ) + if (customRe[customRe.length - 1] == '\\') + customRe = customRe.slice(0, -1) + char; + else + state = 3 /* TokenizerState.ParamRegExpEnd */; + } + else { + customRe += char; + } + break; + case 3 /* TokenizerState.ParamRegExpEnd */: + // same as finalizing a param + consumeBuffer(); + state = 0 /* TokenizerState.Static */; + // go back one character if we were not modifying + if (char !== '*' && char !== '?' && char !== '+') + i--; + customRe = ''; + break; + default: + crash('Unknown state'); + break; + } + } + if (state === 2 /* TokenizerState.ParamRegExp */) + crash(`Unfinished custom RegExp for param "${buffer}"`); + consumeBuffer(); + finalizeSegment(); + // tokenCache.set(path, tokens) + return tokens; + } + + function createRouteRecordMatcher(record, parent, options) { + const parser = tokensToParser(tokenizePath(record.path), options); + // warn against params with the same name + { + const existingKeys = new Set(); + for (const key of parser.keys) { + if (existingKeys.has(key.name)) + warn(`Found duplicated params with name "${key.name}" for path "${record.path}". Only the last one will be available on "$route.params".`); + existingKeys.add(key.name); + } + } + const matcher = assign(parser, { + record, + parent, + // these needs to be populated by the parent + children: [], + alias: [], + }); + if (parent) { + // both are aliases or both are not aliases + // we don't want to mix them because the order is used when + // passing originalRecord in Matcher.addRoute + if (!matcher.record.aliasOf === !parent.record.aliasOf) + parent.children.push(matcher); + } + return matcher; + } + + /** + * Creates a Router Matcher. + * + * @internal + * @param routes - array of initial routes + * @param globalOptions - global route options + */ + function createRouterMatcher(routes, globalOptions) { + // normalized ordered array of matchers + const matchers = []; + const matcherMap = new Map(); + globalOptions = mergeOptions({ strict: false, end: true, sensitive: false }, globalOptions); + function getRecordMatcher(name) { + return matcherMap.get(name); + } + function addRoute(record, parent, originalRecord) { + // used later on to remove by name + const isRootAdd = !originalRecord; + const mainNormalizedRecord = normalizeRouteRecord(record); + { + checkChildMissingNameWithEmptyPath(mainNormalizedRecord, parent); + } + // we might be the child of an alias + mainNormalizedRecord.aliasOf = originalRecord && originalRecord.record; + const options = mergeOptions(globalOptions, record); + // generate an array of records to correctly handle aliases + const normalizedRecords = [ + mainNormalizedRecord, + ]; + if ('alias' in record) { + const aliases = typeof record.alias === 'string' ? [record.alias] : record.alias; + for (const alias of aliases) { + normalizedRecords.push(assign({}, mainNormalizedRecord, { + // this allows us to hold a copy of the `components` option + // so that async components cache is hold on the original record + components: originalRecord + ? originalRecord.record.components + : mainNormalizedRecord.components, + path: alias, + // we might be the child of an alias + aliasOf: originalRecord + ? originalRecord.record + : mainNormalizedRecord, + // the aliases are always of the same kind as the original since they + // are defined on the same record + })); + } + } + let matcher; + let originalMatcher; + for (const normalizedRecord of normalizedRecords) { + const { path } = normalizedRecord; + // Build up the path for nested routes if the child isn't an absolute + // route. Only add the / delimiter if the child path isn't empty and if the + // parent path doesn't have a trailing slash + if (parent && path[0] !== '/') { + const parentPath = parent.record.path; + const connectingSlash = parentPath[parentPath.length - 1] === '/' ? '' : '/'; + normalizedRecord.path = + parent.record.path + (path && connectingSlash + path); + } + if (normalizedRecord.path === '*') { + throw new Error('Catch all routes ("*") must now be defined using a param with a custom regexp.\n' + + 'See more at https://next.router.vuejs.org/guide/migration/#removed-star-or-catch-all-routes.'); + } + // create the object beforehand, so it can be passed to children + matcher = createRouteRecordMatcher(normalizedRecord, parent, options); + if (parent && path[0] === '/') + checkMissingParamsInAbsolutePath(matcher, parent); + // if we are an alias we must tell the original record that we exist, + // so we can be removed + if (originalRecord) { + originalRecord.alias.push(matcher); + { + checkSameParams(originalRecord, matcher); + } + } + else { + // otherwise, the first record is the original and others are aliases + originalMatcher = originalMatcher || matcher; + if (originalMatcher !== matcher) + originalMatcher.alias.push(matcher); + // remove the route if named and only for the top record (avoid in nested calls) + // this works because the original record is the first one + if (isRootAdd && record.name && !isAliasRecord(matcher)) + removeRoute(record.name); + } + if (mainNormalizedRecord.children) { + const children = mainNormalizedRecord.children; + for (let i = 0; i < children.length; i++) { + addRoute(children[i], matcher, originalRecord && originalRecord.children[i]); + } + } + // if there was no original record, then the first one was not an alias and all + // other aliases (if any) need to reference this record when adding children + originalRecord = originalRecord || matcher; + // TODO: add normalized records for more flexibility + // if (parent && isAliasRecord(originalRecord)) { + // parent.children.push(originalRecord) + // } + // Avoid adding a record that doesn't display anything. This allows passing through records without a component to + // not be reached and pass through the catch all route + if ((matcher.record.components && + Object.keys(matcher.record.components).length) || + matcher.record.name || + matcher.record.redirect) { + insertMatcher(matcher); + } + } + return originalMatcher + ? () => { + // since other matchers are aliases, they should be removed by the original matcher + removeRoute(originalMatcher); + } + : noop; + } + function removeRoute(matcherRef) { + if (isRouteName(matcherRef)) { + const matcher = matcherMap.get(matcherRef); + if (matcher) { + matcherMap.delete(matcherRef); + matchers.splice(matchers.indexOf(matcher), 1); + matcher.children.forEach(removeRoute); + matcher.alias.forEach(removeRoute); + } + } + else { + const index = matchers.indexOf(matcherRef); + if (index > -1) { + matchers.splice(index, 1); + if (matcherRef.record.name) + matcherMap.delete(matcherRef.record.name); + matcherRef.children.forEach(removeRoute); + matcherRef.alias.forEach(removeRoute); + } + } + } + function getRoutes() { + return matchers; + } + function insertMatcher(matcher) { + let i = 0; + while (i < matchers.length && + comparePathParserScore(matcher, matchers[i]) >= 0 && + // Adding children with empty path should still appear before the parent + // https://github.com/vuejs/router/issues/1124 + (matcher.record.path !== matchers[i].record.path || + !isRecordChildOf(matcher, matchers[i]))) + i++; + matchers.splice(i, 0, matcher); + // only add the original record to the name map + if (matcher.record.name && !isAliasRecord(matcher)) + matcherMap.set(matcher.record.name, matcher); + } + function resolve(location, currentLocation) { + let matcher; + let params = {}; + let path; + let name; + if ('name' in location && location.name) { + matcher = matcherMap.get(location.name); + if (!matcher) + throw createRouterError(1 /* ErrorTypes.MATCHER_NOT_FOUND */, { + location, + }); + // warn if the user is passing invalid params so they can debug it better when they get removed + { + const invalidParams = Object.keys(location.params || {}).filter(paramName => !matcher.keys.find(k => k.name === paramName)); + if (invalidParams.length) { + warn(`Discarded invalid param(s) "${invalidParams.join('", "')}" when navigating. See https://github.com/vuejs/router/blob/main/packages/router/CHANGELOG.md#414-2022-08-22 for more details.`); + } + } + name = matcher.record.name; + params = assign( + // paramsFromLocation is a new object + paramsFromLocation(currentLocation.params, + // only keep params that exist in the resolved location + // TODO: only keep optional params coming from a parent record + matcher.keys.filter(k => !k.optional).map(k => k.name)), + // discard any existing params in the current location that do not exist here + // #1497 this ensures better active/exact matching + location.params && + paramsFromLocation(location.params, matcher.keys.map(k => k.name))); + // throws if cannot be stringified + path = matcher.stringify(params); + } + else if ('path' in location) { + // no need to resolve the path with the matcher as it was provided + // this also allows the user to control the encoding + path = location.path; + if (!path.startsWith('/')) { + warn(`The Matcher cannot resolve relative paths but received "${path}". Unless you directly called \`matcher.resolve("${path}")\`, this is probably a bug in vue-router. Please open an issue at https://new-issue.vuejs.org/?repo=vuejs/router.`); + } + matcher = matchers.find(m => m.re.test(path)); + // matcher should have a value after the loop + if (matcher) { + // we know the matcher works because we tested the regexp + params = matcher.parse(path); + name = matcher.record.name; + } + // location is a relative path + } + else { + // match by name or path of current route + matcher = currentLocation.name + ? matcherMap.get(currentLocation.name) + : matchers.find(m => m.re.test(currentLocation.path)); + if (!matcher) + throw createRouterError(1 /* ErrorTypes.MATCHER_NOT_FOUND */, { + location, + currentLocation, + }); + name = matcher.record.name; + // since we are navigating to the same location, we don't need to pick the + // params like when `name` is provided + params = assign({}, currentLocation.params, location.params); + path = matcher.stringify(params); + } + const matched = []; + let parentMatcher = matcher; + while (parentMatcher) { + // reversed order so parents are at the beginning + matched.unshift(parentMatcher.record); + parentMatcher = parentMatcher.parent; + } + return { + name, + path, + params, + matched, + meta: mergeMetaFields(matched), + }; + } + // add initial routes + routes.forEach(route => addRoute(route)); + return { addRoute, resolve, removeRoute, getRoutes, getRecordMatcher }; + } + function paramsFromLocation(params, keys) { + const newParams = {}; + for (const key of keys) { + if (key in params) + newParams[key] = params[key]; + } + return newParams; + } + /** + * Normalizes a RouteRecordRaw. Creates a copy + * + * @param record + * @returns the normalized version + */ + function normalizeRouteRecord(record) { + return { + path: record.path, + redirect: record.redirect, + name: record.name, + meta: record.meta || {}, + aliasOf: undefined, + beforeEnter: record.beforeEnter, + props: normalizeRecordProps(record), + children: record.children || [], + instances: {}, + leaveGuards: new Set(), + updateGuards: new Set(), + enterCallbacks: {}, + components: 'components' in record + ? record.components || null + : record.component && { default: record.component }, + }; + } + /** + * Normalize the optional `props` in a record to always be an object similar to + * components. Also accept a boolean for components. + * @param record + */ + function normalizeRecordProps(record) { + const propsObject = {}; + // props does not exist on redirect records, but we can set false directly + const props = record.props || false; + if ('component' in record) { + propsObject.default = props; + } + else { + // NOTE: we could also allow a function to be applied to every component. + // Would need user feedback for use cases + for (const name in record.components) + propsObject[name] = typeof props === 'boolean' ? props : props[name]; + } + return propsObject; + } + /** + * Checks if a record or any of its parent is an alias + * @param record + */ + function isAliasRecord(record) { + while (record) { + if (record.record.aliasOf) + return true; + record = record.parent; + } + return false; + } + /** + * Merge meta fields of an array of records + * + * @param matched - array of matched records + */ + function mergeMetaFields(matched) { + return matched.reduce((meta, record) => assign(meta, record.meta), {}); + } + function mergeOptions(defaults, partialOptions) { + const options = {}; + for (const key in defaults) { + options[key] = key in partialOptions ? partialOptions[key] : defaults[key]; + } + return options; + } + function isSameParam(a, b) { + return (a.name === b.name && + a.optional === b.optional && + a.repeatable === b.repeatable); + } + /** + * Check if a path and its alias have the same required params + * + * @param a - original record + * @param b - alias record + */ + function checkSameParams(a, b) { + for (const key of a.keys) { + if (!key.optional && !b.keys.find(isSameParam.bind(null, key))) + return warn(`Alias "${b.record.path}" and the original record: "${a.record.path}" must have the exact same param named "${key.name}"`); + } + for (const key of b.keys) { + if (!key.optional && !a.keys.find(isSameParam.bind(null, key))) + return warn(`Alias "${b.record.path}" and the original record: "${a.record.path}" must have the exact same param named "${key.name}"`); + } + } + /** + * A route with a name and a child with an empty path without a name should warn when adding the route + * + * @param mainNormalizedRecord - RouteRecordNormalized + * @param parent - RouteRecordMatcher + */ + function checkChildMissingNameWithEmptyPath(mainNormalizedRecord, parent) { + if (parent && + parent.record.name && + !mainNormalizedRecord.name && + !mainNormalizedRecord.path) { + warn(`The route named "${String(parent.record.name)}" has a child without a name and an empty path. Using that name won't render the empty path child so you probably want to move the name to the child instead. If this is intentional, add a name to the child route to remove the warning.`); + } + } + function checkMissingParamsInAbsolutePath(record, parent) { + for (const key of parent.keys) { + if (!record.keys.find(isSameParam.bind(null, key))) + return warn(`Absolute path "${record.record.path}" must have the exact same param named "${key.name}" as its parent "${parent.record.path}".`); + } + } + function isRecordChildOf(record, parent) { + return parent.children.some(child => child === record || isRecordChildOf(record, child)); + } + + /** + * Encoding Rules ␣ = Space Path: ␣ " < > # ? { } Query: ␣ " < > # & = Hash: ␣ " + * < > ` + * + * On top of that, the RFC3986 (https://tools.ietf.org/html/rfc3986#section-2.2) + * defines some extra characters to be encoded. Most browsers do not encode them + * in encodeURI https://github.com/whatwg/url/issues/369, so it may be safer to + * also encode `!'()*`. Leaving un-encoded only ASCII alphanumeric(`a-zA-Z0-9`) + * plus `-._~`. This extra safety should be applied to query by patching the + * string returned by encodeURIComponent encodeURI also encodes `[\]^`. `\` + * should be encoded to avoid ambiguity. Browsers (IE, FF, C) transform a `\` + * into a `/` if directly typed in. The _backtick_ (`````) should also be + * encoded everywhere because some browsers like FF encode it when directly + * written while others don't. Safari and IE don't encode ``"<>{}``` in hash. + */ + // const EXTRA_RESERVED_RE = /[!'()*]/g + // const encodeReservedReplacer = (c: string) => '%' + c.charCodeAt(0).toString(16) + const HASH_RE = /#/g; // %23 + const AMPERSAND_RE = /&/g; // %26 + const SLASH_RE = /\//g; // %2F + const EQUAL_RE = /=/g; // %3D + const IM_RE = /\?/g; // %3F + const PLUS_RE = /\+/g; // %2B + /** + * NOTE: It's not clear to me if we should encode the + symbol in queries, it + * seems to be less flexible than not doing so and I can't find out the legacy + * systems requiring this for regular requests like text/html. In the standard, + * the encoding of the plus character is only mentioned for + * application/x-www-form-urlencoded + * (https://url.spec.whatwg.org/#urlencoded-parsing) and most browsers seems lo + * leave the plus character as is in queries. To be more flexible, we allow the + * plus character on the query, but it can also be manually encoded by the user. + * + * Resources: + * - https://url.spec.whatwg.org/#urlencoded-parsing + * - https://stackoverflow.com/questions/1634271/url-encoding-the-space-character-or-20 + */ + const ENC_BRACKET_OPEN_RE = /%5B/g; // [ + const ENC_BRACKET_CLOSE_RE = /%5D/g; // ] + const ENC_CARET_RE = /%5E/g; // ^ + const ENC_BACKTICK_RE = /%60/g; // ` + const ENC_CURLY_OPEN_RE = /%7B/g; // { + const ENC_PIPE_RE = /%7C/g; // | + const ENC_CURLY_CLOSE_RE = /%7D/g; // } + const ENC_SPACE_RE = /%20/g; // } + /** + * Encode characters that need to be encoded on the path, search and hash + * sections of the URL. + * + * @internal + * @param text - string to encode + * @returns encoded string + */ + function commonEncode(text) { + return encodeURI('' + text) + .replace(ENC_PIPE_RE, '|') + .replace(ENC_BRACKET_OPEN_RE, '[') + .replace(ENC_BRACKET_CLOSE_RE, ']'); + } + /** + * Encode characters that need to be encoded on the hash section of the URL. + * + * @param text - string to encode + * @returns encoded string + */ + function encodeHash(text) { + return commonEncode(text) + .replace(ENC_CURLY_OPEN_RE, '{') + .replace(ENC_CURLY_CLOSE_RE, '}') + .replace(ENC_CARET_RE, '^'); + } + /** + * Encode characters that need to be encoded query values on the query + * section of the URL. + * + * @param text - string to encode + * @returns encoded string + */ + function encodeQueryValue(text) { + return (commonEncode(text) + // Encode the space as +, encode the + to differentiate it from the space + .replace(PLUS_RE, '%2B') + .replace(ENC_SPACE_RE, '+') + .replace(HASH_RE, '%23') + .replace(AMPERSAND_RE, '%26') + .replace(ENC_BACKTICK_RE, '`') + .replace(ENC_CURLY_OPEN_RE, '{') + .replace(ENC_CURLY_CLOSE_RE, '}') + .replace(ENC_CARET_RE, '^')); + } + /** + * Like `encodeQueryValue` but also encodes the `=` character. + * + * @param text - string to encode + */ + function encodeQueryKey(text) { + return encodeQueryValue(text).replace(EQUAL_RE, '%3D'); + } + /** + * Encode characters that need to be encoded on the path section of the URL. + * + * @param text - string to encode + * @returns encoded string + */ + function encodePath(text) { + return commonEncode(text).replace(HASH_RE, '%23').replace(IM_RE, '%3F'); + } + /** + * Encode characters that need to be encoded on the path section of the URL as a + * param. This function encodes everything {@link encodePath} does plus the + * slash (`/`) character. If `text` is `null` or `undefined`, returns an empty + * string instead. + * + * @param text - string to encode + * @returns encoded string + */ + function encodeParam(text) { + return text == null ? '' : encodePath(text).replace(SLASH_RE, '%2F'); + } + /** + * Decode text using `decodeURIComponent`. Returns the original text if it + * fails. + * + * @param text - string to decode + * @returns decoded string + */ + function decode(text) { + try { + return decodeURIComponent('' + text); + } + catch (err) { + warn(`Error decoding "${text}". Using original value`); + } + return '' + text; + } + + /** + * Transforms a queryString into a {@link LocationQuery} object. Accept both, a + * version with the leading `?` and without Should work as URLSearchParams + + * @internal + * + * @param search - search string to parse + * @returns a query object + */ + function parseQuery(search) { + const query = {}; + // avoid creating an object with an empty key and empty value + // because of split('&') + if (search === '' || search === '?') + return query; + const hasLeadingIM = search[0] === '?'; + const searchParams = (hasLeadingIM ? search.slice(1) : search).split('&'); + for (let i = 0; i < searchParams.length; ++i) { + // pre decode the + into space + const searchParam = searchParams[i].replace(PLUS_RE, ' '); + // allow the = character + const eqPos = searchParam.indexOf('='); + const key = decode(eqPos < 0 ? searchParam : searchParam.slice(0, eqPos)); + const value = eqPos < 0 ? null : decode(searchParam.slice(eqPos + 1)); + if (key in query) { + // an extra variable for ts types + let currentValue = query[key]; + if (!isArray(currentValue)) { + currentValue = query[key] = [currentValue]; + } + currentValue.push(value); + } + else { + query[key] = value; + } + } + return query; + } + /** + * Stringifies a {@link LocationQueryRaw} object. Like `URLSearchParams`, it + * doesn't prepend a `?` + * + * @internal + * + * @param query - query object to stringify + * @returns string version of the query without the leading `?` + */ + function stringifyQuery(query) { + let search = ''; + for (let key in query) { + const value = query[key]; + key = encodeQueryKey(key); + if (value == null) { + // only null adds the value + if (value !== undefined) { + search += (search.length ? '&' : '') + key; + } + continue; + } + // keep null values + const values = isArray(value) + ? value.map(v => v && encodeQueryValue(v)) + : [value && encodeQueryValue(value)]; + values.forEach(value => { + // skip undefined values in arrays as if they were not present + // smaller code than using filter + if (value !== undefined) { + // only append & with non-empty search + search += (search.length ? '&' : '') + key; + if (value != null) + search += '=' + value; + } + }); + } + return search; + } + /** + * Transforms a {@link LocationQueryRaw} into a {@link LocationQuery} by casting + * numbers into strings, removing keys with an undefined value and replacing + * undefined with null in arrays + * + * @param query - query object to normalize + * @returns a normalized query object + */ + function normalizeQuery(query) { + const normalizedQuery = {}; + for (const key in query) { + const value = query[key]; + if (value !== undefined) { + normalizedQuery[key] = isArray(value) + ? value.map(v => (v == null ? null : '' + v)) + : value == null + ? value + : '' + value; + } + } + return normalizedQuery; + } + + /** + * RouteRecord being rendered by the closest ancestor Router View. Used for + * `onBeforeRouteUpdate` and `onBeforeRouteLeave`. rvlm stands for Router View + * Location Matched + * + * @internal + */ + const matchedRouteKey = Symbol('router view location matched' ); + /** + * Allows overriding the router view depth to control which component in + * `matched` is rendered. rvd stands for Router View Depth + * + * @internal + */ + const viewDepthKey = Symbol('router view depth' ); + /** + * Allows overriding the router instance returned by `useRouter` in tests. r + * stands for router + * + * @internal + */ + const routerKey = Symbol('router' ); + /** + * Allows overriding the current route returned by `useRoute` in tests. rl + * stands for route location + * + * @internal + */ + const routeLocationKey = Symbol('route location' ); + /** + * Allows overriding the current route used by router-view. Internally this is + * used when the `route` prop is passed. + * + * @internal + */ + const routerViewLocationKey = Symbol('router view location' ); + + /** + * Create a list of callbacks that can be reset. Used to create before and after navigation guards list + */ + function useCallbacks() { + let handlers = []; + function add(handler) { + handlers.push(handler); + return () => { + const i = handlers.indexOf(handler); + if (i > -1) + handlers.splice(i, 1); + }; + } + function reset() { + handlers = []; + } + return { + add, + list: () => handlers, + reset, + }; + } + + function registerGuard(record, name, guard) { + const removeFromList = () => { + record[name].delete(guard); + }; + vue.onUnmounted(removeFromList); + vue.onDeactivated(removeFromList); + vue.onActivated(() => { + record[name].add(guard); + }); + record[name].add(guard); + } + /** + * Add a navigation guard that triggers whenever the component for the current + * location is about to be left. Similar to {@link beforeRouteLeave} but can be + * used in any component. The guard is removed when the component is unmounted. + * + * @param leaveGuard - {@link NavigationGuard} + */ + function onBeforeRouteLeave(leaveGuard) { + if (!vue.getCurrentInstance()) { + warn('getCurrentInstance() returned null. onBeforeRouteLeave() must be called at the top of a setup function'); + return; + } + const activeRecord = vue.inject(matchedRouteKey, + // to avoid warning + {}).value; + if (!activeRecord) { + warn('No active route record was found when calling `onBeforeRouteLeave()`. Make sure you call this function inside a component child of . Maybe you called it inside of App.vue?'); + return; + } + registerGuard(activeRecord, 'leaveGuards', leaveGuard); + } + /** + * Add a navigation guard that triggers whenever the current location is about + * to be updated. Similar to {@link beforeRouteUpdate} but can be used in any + * component. The guard is removed when the component is unmounted. + * + * @param updateGuard - {@link NavigationGuard} + */ + function onBeforeRouteUpdate(updateGuard) { + if (!vue.getCurrentInstance()) { + warn('getCurrentInstance() returned null. onBeforeRouteUpdate() must be called at the top of a setup function'); + return; + } + const activeRecord = vue.inject(matchedRouteKey, + // to avoid warning + {}).value; + if (!activeRecord) { + warn('No active route record was found when calling `onBeforeRouteUpdate()`. Make sure you call this function inside a component child of . Maybe you called it inside of App.vue?'); + return; + } + registerGuard(activeRecord, 'updateGuards', updateGuard); + } + function guardToPromiseFn(guard, to, from, record, name) { + // keep a reference to the enterCallbackArray to prevent pushing callbacks if a new navigation took place + const enterCallbackArray = record && + // name is defined if record is because of the function overload + (record.enterCallbacks[name] = record.enterCallbacks[name] || []); + return () => new Promise((resolve, reject) => { + const next = (valid) => { + if (valid === false) { + reject(createRouterError(4 /* ErrorTypes.NAVIGATION_ABORTED */, { + from, + to, + })); + } + else if (valid instanceof Error) { + reject(valid); + } + else if (isRouteLocation(valid)) { + reject(createRouterError(2 /* ErrorTypes.NAVIGATION_GUARD_REDIRECT */, { + from: to, + to: valid, + })); + } + else { + if (enterCallbackArray && + // since enterCallbackArray is truthy, both record and name also are + record.enterCallbacks[name] === enterCallbackArray && + typeof valid === 'function') { + enterCallbackArray.push(valid); + } + resolve(); + } + }; + // wrapping with Promise.resolve allows it to work with both async and sync guards + const guardReturn = guard.call(record && record.instances[name], to, from, canOnlyBeCalledOnce(next, to, from) ); + let guardCall = Promise.resolve(guardReturn); + if (guard.length < 3) + guardCall = guardCall.then(next); + if (guard.length > 2) { + const message = `The "next" callback was never called inside of ${guard.name ? '"' + guard.name + '"' : ''}:\n${guard.toString()}\n. If you are returning a value instead of calling "next", make sure to remove the "next" parameter from your function.`; + if (typeof guardReturn === 'object' && 'then' in guardReturn) { + guardCall = guardCall.then(resolvedValue => { + // @ts-expect-error: _called is added at canOnlyBeCalledOnce + if (!next._called) { + warn(message); + return Promise.reject(new Error('Invalid navigation guard')); + } + return resolvedValue; + }); + } + else if (guardReturn !== undefined) { + // @ts-expect-error: _called is added at canOnlyBeCalledOnce + if (!next._called) { + warn(message); + reject(new Error('Invalid navigation guard')); + return; + } + } + } + guardCall.catch(err => reject(err)); + }); + } + function canOnlyBeCalledOnce(next, to, from) { + let called = 0; + return function () { + if (called++ === 1) + warn(`The "next" callback was called more than once in one navigation guard when going from "${from.fullPath}" to "${to.fullPath}". It should be called exactly one time in each navigation guard. This will fail in production.`); + // @ts-expect-error: we put it in the original one because it's easier to check + next._called = true; + if (called === 1) + next.apply(null, arguments); + }; + } + function extractComponentsGuards(matched, guardType, to, from) { + const guards = []; + for (const record of matched) { + if (!record.components && !record.children.length) { + warn(`Record with path "${record.path}" is either missing a "component(s)"` + + ` or "children" property.`); + } + for (const name in record.components) { + let rawComponent = record.components[name]; + { + if (!rawComponent || + (typeof rawComponent !== 'object' && + typeof rawComponent !== 'function')) { + warn(`Component "${name}" in record with path "${record.path}" is not` + + ` a valid component. Received "${String(rawComponent)}".`); + // throw to ensure we stop here but warn to ensure the message isn't + // missed by the user + throw new Error('Invalid route component'); + } + else if ('then' in rawComponent) { + // warn if user wrote import('/component.vue') instead of () => + // import('./component.vue') + warn(`Component "${name}" in record with path "${record.path}" is a ` + + `Promise instead of a function that returns a Promise. Did you ` + + `write "import('./MyPage.vue')" instead of ` + + `"() => import('./MyPage.vue')" ? This will break in ` + + `production if not fixed.`); + const promise = rawComponent; + rawComponent = () => promise; + } + else if (rawComponent.__asyncLoader && + // warn only once per component + !rawComponent.__warnedDefineAsync) { + rawComponent.__warnedDefineAsync = true; + warn(`Component "${name}" in record with path "${record.path}" is defined ` + + `using "defineAsyncComponent()". ` + + `Write "() => import('./MyPage.vue')" instead of ` + + `"defineAsyncComponent(() => import('./MyPage.vue'))".`); + } + } + // skip update and leave guards if the route component is not mounted + if (guardType !== 'beforeRouteEnter' && !record.instances[name]) + continue; + if (isRouteComponent(rawComponent)) { + // __vccOpts is added by vue-class-component and contain the regular options + const options = rawComponent.__vccOpts || rawComponent; + const guard = options[guardType]; + guard && guards.push(guardToPromiseFn(guard, to, from, record, name)); + } + else { + // start requesting the chunk already + let componentPromise = rawComponent(); + if (!('catch' in componentPromise)) { + warn(`Component "${name}" in record with path "${record.path}" is a function that does not return a Promise. If you were passing a functional component, make sure to add a "displayName" to the component. This will break in production if not fixed.`); + componentPromise = Promise.resolve(componentPromise); + } + guards.push(() => componentPromise.then(resolved => { + if (!resolved) + return Promise.reject(new Error(`Couldn't resolve component "${name}" at "${record.path}"`)); + const resolvedComponent = isESModule(resolved) + ? resolved.default + : resolved; + // replace the function with the resolved component + // cannot be null or undefined because we went into the for loop + record.components[name] = resolvedComponent; + // __vccOpts is added by vue-class-component and contain the regular options + const options = resolvedComponent.__vccOpts || resolvedComponent; + const guard = options[guardType]; + return guard && guardToPromiseFn(guard, to, from, record, name)(); + })); + } + } + } + return guards; + } + /** + * Allows differentiating lazy components from functional components and vue-class-component + * @internal + * + * @param component + */ + function isRouteComponent(component) { + return (typeof component === 'object' || + 'displayName' in component || + 'props' in component || + '__vccOpts' in component); + } + /** + * Ensures a route is loaded, so it can be passed as o prop to ``. + * + * @param route - resolved route to load + */ + function loadRouteLocation(route) { + return route.matched.every(record => record.redirect) + ? Promise.reject(new Error('Cannot load a route that redirects.')) + : Promise.all(route.matched.map(record => record.components && + Promise.all(Object.keys(record.components).reduce((promises, name) => { + const rawComponent = record.components[name]; + if (typeof rawComponent === 'function' && + !('displayName' in rawComponent)) { + promises.push(rawComponent().then(resolved => { + if (!resolved) + return Promise.reject(new Error(`Couldn't resolve component "${name}" at "${record.path}". Ensure you passed a function that returns a promise.`)); + const resolvedComponent = isESModule(resolved) + ? resolved.default + : resolved; + // replace the function with the resolved component + // cannot be null or undefined because we went into the for loop + record.components[name] = resolvedComponent; + return; + })); + } + return promises; + }, [])))).then(() => route); + } + + // TODO: we could allow currentRoute as a prop to expose `isActive` and + // `isExactActive` behavior should go through an RFC + function useLink(props) { + const router = vue.inject(routerKey); + const currentRoute = vue.inject(routeLocationKey); + const route = vue.computed(() => router.resolve(vue.unref(props.to))); + const activeRecordIndex = vue.computed(() => { + const { matched } = route.value; + const { length } = matched; + const routeMatched = matched[length - 1]; + const currentMatched = currentRoute.matched; + if (!routeMatched || !currentMatched.length) + return -1; + const index = currentMatched.findIndex(isSameRouteRecord.bind(null, routeMatched)); + if (index > -1) + return index; + // possible parent record + const parentRecordPath = getOriginalPath(matched[length - 2]); + return ( + // we are dealing with nested routes + length > 1 && + // if the parent and matched route have the same path, this link is + // referring to the empty child. Or we currently are on a different + // child of the same parent + getOriginalPath(routeMatched) === parentRecordPath && + // avoid comparing the child with its parent + currentMatched[currentMatched.length - 1].path !== parentRecordPath + ? currentMatched.findIndex(isSameRouteRecord.bind(null, matched[length - 2])) + : index); + }); + const isActive = vue.computed(() => activeRecordIndex.value > -1 && + includesParams(currentRoute.params, route.value.params)); + const isExactActive = vue.computed(() => activeRecordIndex.value > -1 && + activeRecordIndex.value === currentRoute.matched.length - 1 && + isSameRouteLocationParams(currentRoute.params, route.value.params)); + function navigate(e = {}) { + if (guardEvent(e)) { + return router[vue.unref(props.replace) ? 'replace' : 'push'](vue.unref(props.to) + // avoid uncaught errors are they are logged anyway + ).catch(noop); + } + return Promise.resolve(); + } + // devtools only + if (isBrowser) { + const instance = vue.getCurrentInstance(); + if (instance) { + const linkContextDevtools = { + route: route.value, + isActive: isActive.value, + isExactActive: isExactActive.value, + }; + // @ts-expect-error: this is internal + instance.__vrl_devtools = instance.__vrl_devtools || []; + // @ts-expect-error: this is internal + instance.__vrl_devtools.push(linkContextDevtools); + vue.watchEffect(() => { + linkContextDevtools.route = route.value; + linkContextDevtools.isActive = isActive.value; + linkContextDevtools.isExactActive = isExactActive.value; + }, { flush: 'post' }); + } + } + /** + * NOTE: update {@link _RouterLinkI}'s `$slots` type when updating this + */ + return { + route, + href: vue.computed(() => route.value.href), + isActive, + isExactActive, + navigate, + }; + } + const RouterLinkImpl = /*#__PURE__*/ vue.defineComponent({ + name: 'RouterLink', + compatConfig: { MODE: 3 }, + props: { + to: { + type: [String, Object], + required: true, + }, + replace: Boolean, + activeClass: String, + // inactiveClass: String, + exactActiveClass: String, + custom: Boolean, + ariaCurrentValue: { + type: String, + default: 'page', + }, + }, + useLink, + setup(props, { slots }) { + const link = vue.reactive(useLink(props)); + const { options } = vue.inject(routerKey); + const elClass = vue.computed(() => ({ + [getLinkClass(props.activeClass, options.linkActiveClass, 'router-link-active')]: link.isActive, + // [getLinkClass( + // props.inactiveClass, + // options.linkInactiveClass, + // 'router-link-inactive' + // )]: !link.isExactActive, + [getLinkClass(props.exactActiveClass, options.linkExactActiveClass, 'router-link-exact-active')]: link.isExactActive, + })); + return () => { + const children = slots.default && slots.default(link); + return props.custom + ? children + : vue.h('a', { + 'aria-current': link.isExactActive + ? props.ariaCurrentValue + : null, + href: link.href, + // this would override user added attrs but Vue will still add + // the listener, so we end up triggering both + onClick: link.navigate, + class: elClass.value, + }, children); + }; + }, + }); + // export the public type for h/tsx inference + // also to avoid inline import() in generated d.ts files + /** + * Component to render a link that triggers a navigation on click. + */ + const RouterLink = RouterLinkImpl; + function guardEvent(e) { + // don't redirect with control keys + if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) + return; + // don't redirect when preventDefault called + if (e.defaultPrevented) + return; + // don't redirect on right click + if (e.button !== undefined && e.button !== 0) + return; + // don't redirect if `target="_blank"` + // @ts-expect-error getAttribute does exist + if (e.currentTarget && e.currentTarget.getAttribute) { + // @ts-expect-error getAttribute exists + const target = e.currentTarget.getAttribute('target'); + if (/\b_blank\b/i.test(target)) + return; + } + // this may be a Weex event which doesn't have this method + if (e.preventDefault) + e.preventDefault(); + return true; + } + function includesParams(outer, inner) { + for (const key in inner) { + const innerValue = inner[key]; + const outerValue = outer[key]; + if (typeof innerValue === 'string') { + if (innerValue !== outerValue) + return false; + } + else { + if (!isArray(outerValue) || + outerValue.length !== innerValue.length || + innerValue.some((value, i) => value !== outerValue[i])) + return false; + } + } + return true; + } + /** + * Get the original path value of a record by following its aliasOf + * @param record + */ + function getOriginalPath(record) { + return record ? (record.aliasOf ? record.aliasOf.path : record.path) : ''; + } + /** + * Utility class to get the active class based on defaults. + * @param propClass + * @param globalClass + * @param defaultClass + */ + const getLinkClass = (propClass, globalClass, defaultClass) => propClass != null + ? propClass + : globalClass != null + ? globalClass + : defaultClass; + + const RouterViewImpl = /*#__PURE__*/ vue.defineComponent({ + name: 'RouterView', + // #674 we manually inherit them + inheritAttrs: false, + props: { + name: { + type: String, + default: 'default', + }, + route: Object, + }, + // Better compat for @vue/compat users + // https://github.com/vuejs/router/issues/1315 + compatConfig: { MODE: 3 }, + setup(props, { attrs, slots }) { + warnDeprecatedUsage(); + const injectedRoute = vue.inject(routerViewLocationKey); + const routeToDisplay = vue.computed(() => props.route || injectedRoute.value); + const injectedDepth = vue.inject(viewDepthKey, 0); + // The depth changes based on empty components option, which allows passthrough routes e.g. routes with children + // that are used to reuse the `path` property + const depth = vue.computed(() => { + let initialDepth = vue.unref(injectedDepth); + const { matched } = routeToDisplay.value; + let matchedRoute; + while ((matchedRoute = matched[initialDepth]) && + !matchedRoute.components) { + initialDepth++; + } + return initialDepth; + }); + const matchedRouteRef = vue.computed(() => routeToDisplay.value.matched[depth.value]); + vue.provide(viewDepthKey, vue.computed(() => depth.value + 1)); + vue.provide(matchedRouteKey, matchedRouteRef); + vue.provide(routerViewLocationKey, routeToDisplay); + const viewRef = vue.ref(); + // watch at the same time the component instance, the route record we are + // rendering, and the name + vue.watch(() => [viewRef.value, matchedRouteRef.value, props.name], ([instance, to, name], [oldInstance, from, oldName]) => { + // copy reused instances + if (to) { + // this will update the instance for new instances as well as reused + // instances when navigating to a new route + to.instances[name] = instance; + // the component instance is reused for a different route or name, so + // we copy any saved update or leave guards. With async setup, the + // mounting component will mount before the matchedRoute changes, + // making instance === oldInstance, so we check if guards have been + // added before. This works because we remove guards when + // unmounting/deactivating components + if (from && from !== to && instance && instance === oldInstance) { + if (!to.leaveGuards.size) { + to.leaveGuards = from.leaveGuards; + } + if (!to.updateGuards.size) { + to.updateGuards = from.updateGuards; + } + } + } + // trigger beforeRouteEnter next callbacks + if (instance && + to && + // if there is no instance but to and from are the same this might be + // the first visit + (!from || !isSameRouteRecord(to, from) || !oldInstance)) { + (to.enterCallbacks[name] || []).forEach(callback => callback(instance)); + } + }, { flush: 'post' }); + return () => { + const route = routeToDisplay.value; + // we need the value at the time we render because when we unmount, we + // navigated to a different location so the value is different + const currentName = props.name; + const matchedRoute = matchedRouteRef.value; + const ViewComponent = matchedRoute && matchedRoute.components[currentName]; + if (!ViewComponent) { + return normalizeSlot(slots.default, { Component: ViewComponent, route }); + } + // props from route configuration + const routePropsOption = matchedRoute.props[currentName]; + const routeProps = routePropsOption + ? routePropsOption === true + ? route.params + : typeof routePropsOption === 'function' + ? routePropsOption(route) + : routePropsOption + : null; + const onVnodeUnmounted = vnode => { + // remove the instance reference to prevent leak + if (vnode.component.isUnmounted) { + matchedRoute.instances[currentName] = null; + } + }; + const component = vue.h(ViewComponent, assign({}, routeProps, attrs, { + onVnodeUnmounted, + ref: viewRef, + })); + if (isBrowser && + component.ref) { + // TODO: can display if it's an alias, its props + const info = { + depth: depth.value, + name: matchedRoute.name, + path: matchedRoute.path, + meta: matchedRoute.meta, + }; + const internalInstances = isArray(component.ref) + ? component.ref.map(r => r.i) + : [component.ref.i]; + internalInstances.forEach(instance => { + // @ts-expect-error + instance.__vrv_devtools = info; + }); + } + return ( + // pass the vnode to the slot as a prop. + // h and both accept vnodes + normalizeSlot(slots.default, { Component: component, route }) || + component); + }; + }, + }); + function normalizeSlot(slot, data) { + if (!slot) + return null; + const slotContent = slot(data); + return slotContent.length === 1 ? slotContent[0] : slotContent; + } + // export the public type for h/tsx inference + // also to avoid inline import() in generated d.ts files + /** + * Component to display the current route the user is at. + */ + const RouterView = RouterViewImpl; + // warn against deprecated usage with & + // due to functional component being no longer eager in Vue 3 + function warnDeprecatedUsage() { + const instance = vue.getCurrentInstance(); + const parentName = instance.parent && instance.parent.type.name; + const parentSubTreeType = instance.parent && instance.parent.subTree && instance.parent.subTree.type; + if (parentName && + (parentName === 'KeepAlive' || parentName.includes('Transition')) && + typeof parentSubTreeType === 'object' && + parentSubTreeType.name === 'RouterView') { + const comp = parentName === 'KeepAlive' ? 'keep-alive' : 'transition'; + warn(` can no longer be used directly inside or .\n` + + `Use slot props instead:\n\n` + + `\n` + + ` <${comp}>\n` + + ` \n` + + ` \n` + + ``); + } + } + + function getDevtoolsGlobalHook() { + return getTarget().__VUE_DEVTOOLS_GLOBAL_HOOK__; + } + function getTarget() { + // @ts-ignore + return (typeof navigator !== 'undefined' && typeof window !== 'undefined') + ? window + : typeof global !== 'undefined' + ? global + : {}; + } + const isProxyAvailable = typeof Proxy === 'function'; + + const HOOK_SETUP = 'devtools-plugin:setup'; + const HOOK_PLUGIN_SETTINGS_SET = 'plugin:settings:set'; + + let supported; + let perf; + function isPerformanceSupported() { + var _a; + if (supported !== undefined) { + return supported; + } + if (typeof window !== 'undefined' && window.performance) { + supported = true; + perf = window.performance; + } + else if (typeof global !== 'undefined' && ((_a = global.perf_hooks) === null || _a === void 0 ? void 0 : _a.performance)) { + supported = true; + perf = global.perf_hooks.performance; + } + else { + supported = false; + } + return supported; + } + function now() { + return isPerformanceSupported() ? perf.now() : Date.now(); + } + + class ApiProxy { + constructor(plugin, hook) { + this.target = null; + this.targetQueue = []; + this.onQueue = []; + this.plugin = plugin; + this.hook = hook; + const defaultSettings = {}; + if (plugin.settings) { + for (const id in plugin.settings) { + const item = plugin.settings[id]; + defaultSettings[id] = item.defaultValue; + } + } + const localSettingsSaveId = `__vue-devtools-plugin-settings__${plugin.id}`; + let currentSettings = Object.assign({}, defaultSettings); + try { + const raw = localStorage.getItem(localSettingsSaveId); + const data = JSON.parse(raw); + Object.assign(currentSettings, data); + } + catch (e) { + // noop + } + this.fallbacks = { + getSettings() { + return currentSettings; + }, + setSettings(value) { + try { + localStorage.setItem(localSettingsSaveId, JSON.stringify(value)); + } + catch (e) { + // noop + } + currentSettings = value; + }, + now() { + return now(); + }, + }; + if (hook) { + hook.on(HOOK_PLUGIN_SETTINGS_SET, (pluginId, value) => { + if (pluginId === this.plugin.id) { + this.fallbacks.setSettings(value); + } + }); + } + this.proxiedOn = new Proxy({}, { + get: (_target, prop) => { + if (this.target) { + return this.target.on[prop]; + } + else { + return (...args) => { + this.onQueue.push({ + method: prop, + args, + }); + }; + } + }, + }); + this.proxiedTarget = new Proxy({}, { + get: (_target, prop) => { + if (this.target) { + return this.target[prop]; + } + else if (prop === 'on') { + return this.proxiedOn; + } + else if (Object.keys(this.fallbacks).includes(prop)) { + return (...args) => { + this.targetQueue.push({ + method: prop, + args, + resolve: () => { }, + }); + return this.fallbacks[prop](...args); + }; + } + else { + return (...args) => { + return new Promise(resolve => { + this.targetQueue.push({ + method: prop, + args, + resolve, + }); + }); + }; + } + }, + }); + } + async setRealTarget(target) { + this.target = target; + for (const item of this.onQueue) { + this.target.on[item.method](...item.args); + } + for (const item of this.targetQueue) { + item.resolve(await this.target[item.method](...item.args)); + } + } + } + + function setupDevtoolsPlugin(pluginDescriptor, setupFn) { + const descriptor = pluginDescriptor; + const target = getTarget(); + const hook = getDevtoolsGlobalHook(); + const enableProxy = isProxyAvailable && descriptor.enableEarlyProxy; + if (hook && (target.__VUE_DEVTOOLS_PLUGIN_API_AVAILABLE__ || !enableProxy)) { + hook.emit(HOOK_SETUP, pluginDescriptor, setupFn); + } + else { + const proxy = enableProxy ? new ApiProxy(descriptor, hook) : null; + const list = target.__VUE_DEVTOOLS_PLUGINS__ = target.__VUE_DEVTOOLS_PLUGINS__ || []; + list.push({ + pluginDescriptor: descriptor, + setupFn, + proxy, + }); + if (proxy) + setupFn(proxy.proxiedTarget); + } + } + + /** + * Copies a route location and removes any problematic properties that cannot be shown in devtools (e.g. Vue instances). + * + * @param routeLocation - routeLocation to format + * @param tooltip - optional tooltip + * @returns a copy of the routeLocation + */ + function formatRouteLocation(routeLocation, tooltip) { + const copy = assign({}, routeLocation, { + // remove variables that can contain vue instances + matched: routeLocation.matched.map(matched => omit(matched, ['instances', 'children', 'aliasOf'])), + }); + return { + _custom: { + type: null, + readOnly: true, + display: routeLocation.fullPath, + tooltip, + value: copy, + }, + }; + } + function formatDisplay(display) { + return { + _custom: { + display, + }, + }; + } + // to support multiple router instances + let routerId = 0; + function addDevtools(app, router, matcher) { + // Take over router.beforeEach and afterEach + // make sure we are not registering the devtool twice + if (router.__hasDevtools) + return; + router.__hasDevtools = true; + // increment to support multiple router instances + const id = routerId++; + setupDevtoolsPlugin({ + id: 'org.vuejs.router' + (id ? '.' + id : ''), + label: 'Vue Router', + packageName: 'vue-router', + homepage: 'https://router.vuejs.org', + logo: 'https://router.vuejs.org/logo.png', + componentStateTypes: ['Routing'], + app, + }, api => { + if (typeof api.now !== 'function') { + console.warn('[Vue Router]: You seem to be using an outdated version of Vue Devtools. Are you still using the Beta release instead of the stable one? You can find the links at https://devtools.vuejs.org/guide/installation.html.'); + } + // display state added by the router + api.on.inspectComponent((payload, ctx) => { + if (payload.instanceData) { + payload.instanceData.state.push({ + type: 'Routing', + key: '$route', + editable: false, + value: formatRouteLocation(router.currentRoute.value, 'Current Route'), + }); + } + }); + // mark router-link as active and display tags on router views + api.on.visitComponentTree(({ treeNode: node, componentInstance }) => { + if (componentInstance.__vrv_devtools) { + const info = componentInstance.__vrv_devtools; + node.tags.push({ + label: (info.name ? `${info.name.toString()}: ` : '') + info.path, + textColor: 0, + tooltip: 'This component is rendered by <router-view>', + backgroundColor: PINK_500, + }); + } + // if multiple useLink are used + if (isArray(componentInstance.__vrl_devtools)) { + componentInstance.__devtoolsApi = api; + componentInstance.__vrl_devtools.forEach(devtoolsData => { + let backgroundColor = ORANGE_400; + let tooltip = ''; + if (devtoolsData.isExactActive) { + backgroundColor = LIME_500; + tooltip = 'This is exactly active'; + } + else if (devtoolsData.isActive) { + backgroundColor = BLUE_600; + tooltip = 'This link is active'; + } + node.tags.push({ + label: devtoolsData.route.path, + textColor: 0, + tooltip, + backgroundColor, + }); + }); + } + }); + vue.watch(router.currentRoute, () => { + // refresh active state + refreshRoutesView(); + api.notifyComponentUpdate(); + api.sendInspectorTree(routerInspectorId); + api.sendInspectorState(routerInspectorId); + }); + const navigationsLayerId = 'router:navigations:' + id; + api.addTimelineLayer({ + id: navigationsLayerId, + label: `Router${id ? ' ' + id : ''} Navigations`, + color: 0x40a8c4, + }); + // const errorsLayerId = 'router:errors' + // api.addTimelineLayer({ + // id: errorsLayerId, + // label: 'Router Errors', + // color: 0xea5455, + // }) + router.onError((error, to) => { + api.addTimelineEvent({ + layerId: navigationsLayerId, + event: { + title: 'Error during Navigation', + subtitle: to.fullPath, + logType: 'error', + time: api.now(), + data: { error }, + groupId: to.meta.__navigationId, + }, + }); + }); + // attached to `meta` and used to group events + let navigationId = 0; + router.beforeEach((to, from) => { + const data = { + guard: formatDisplay('beforeEach'), + from: formatRouteLocation(from, 'Current Location during this navigation'), + to: formatRouteLocation(to, 'Target location'), + }; + // Used to group navigations together, hide from devtools + Object.defineProperty(to.meta, '__navigationId', { + value: navigationId++, + }); + api.addTimelineEvent({ + layerId: navigationsLayerId, + event: { + time: api.now(), + title: 'Start of navigation', + subtitle: to.fullPath, + data, + groupId: to.meta.__navigationId, + }, + }); + }); + router.afterEach((to, from, failure) => { + const data = { + guard: formatDisplay('afterEach'), + }; + if (failure) { + data.failure = { + _custom: { + type: Error, + readOnly: true, + display: failure ? failure.message : '', + tooltip: 'Navigation Failure', + value: failure, + }, + }; + data.status = formatDisplay('❌'); + } + else { + data.status = formatDisplay('✅'); + } + // we set here to have the right order + data.from = formatRouteLocation(from, 'Current Location during this navigation'); + data.to = formatRouteLocation(to, 'Target location'); + api.addTimelineEvent({ + layerId: navigationsLayerId, + event: { + title: 'End of navigation', + subtitle: to.fullPath, + time: api.now(), + data, + logType: failure ? 'warning' : 'default', + groupId: to.meta.__navigationId, + }, + }); + }); + /** + * Inspector of Existing routes + */ + const routerInspectorId = 'router-inspector:' + id; + api.addInspector({ + id: routerInspectorId, + label: 'Routes' + (id ? ' ' + id : ''), + icon: 'book', + treeFilterPlaceholder: 'Search routes', + }); + function refreshRoutesView() { + // the routes view isn't active + if (!activeRoutesPayload) + return; + const payload = activeRoutesPayload; + // children routes will appear as nested + let routes = matcher.getRoutes().filter(route => !route.parent); + // reset match state to false + routes.forEach(resetMatchStateOnRouteRecord); + // apply a match state if there is a payload + if (payload.filter) { + routes = routes.filter(route => + // save matches state based on the payload + isRouteMatching(route, payload.filter.toLowerCase())); + } + // mark active routes + routes.forEach(route => markRouteRecordActive(route, router.currentRoute.value)); + payload.rootNodes = routes.map(formatRouteRecordForInspector); + } + let activeRoutesPayload; + api.on.getInspectorTree(payload => { + activeRoutesPayload = payload; + if (payload.app === app && payload.inspectorId === routerInspectorId) { + refreshRoutesView(); + } + }); + /** + * Display information about the currently selected route record + */ + api.on.getInspectorState(payload => { + if (payload.app === app && payload.inspectorId === routerInspectorId) { + const routes = matcher.getRoutes(); + const route = routes.find(route => route.record.__vd_id === payload.nodeId); + if (route) { + payload.state = { + options: formatRouteRecordMatcherForStateInspector(route), + }; + } + } + }); + api.sendInspectorTree(routerInspectorId); + api.sendInspectorState(routerInspectorId); + }); + } + function modifierForKey(key) { + if (key.optional) { + return key.repeatable ? '*' : '?'; + } + else { + return key.repeatable ? '+' : ''; + } + } + function formatRouteRecordMatcherForStateInspector(route) { + const { record } = route; + const fields = [ + { editable: false, key: 'path', value: record.path }, + ]; + if (record.name != null) { + fields.push({ + editable: false, + key: 'name', + value: record.name, + }); + } + fields.push({ editable: false, key: 'regexp', value: route.re }); + if (route.keys.length) { + fields.push({ + editable: false, + key: 'keys', + value: { + _custom: { + type: null, + readOnly: true, + display: route.keys + .map(key => `${key.name}${modifierForKey(key)}`) + .join(' '), + tooltip: 'Param keys', + value: route.keys, + }, + }, + }); + } + if (record.redirect != null) { + fields.push({ + editable: false, + key: 'redirect', + value: record.redirect, + }); + } + if (route.alias.length) { + fields.push({ + editable: false, + key: 'aliases', + value: route.alias.map(alias => alias.record.path), + }); + } + if (Object.keys(route.record.meta).length) { + fields.push({ + editable: false, + key: 'meta', + value: route.record.meta, + }); + } + fields.push({ + key: 'score', + editable: false, + value: { + _custom: { + type: null, + readOnly: true, + display: route.score.map(score => score.join(', ')).join(' | '), + tooltip: 'Score used to sort routes', + value: route.score, + }, + }, + }); + return fields; + } + /** + * Extracted from tailwind palette + */ + const PINK_500 = 0xec4899; + const BLUE_600 = 0x2563eb; + const LIME_500 = 0x84cc16; + const CYAN_400 = 0x22d3ee; + const ORANGE_400 = 0xfb923c; + // const GRAY_100 = 0xf4f4f5 + const DARK = 0x666666; + function formatRouteRecordForInspector(route) { + const tags = []; + const { record } = route; + if (record.name != null) { + tags.push({ + label: String(record.name), + textColor: 0, + backgroundColor: CYAN_400, + }); + } + if (record.aliasOf) { + tags.push({ + label: 'alias', + textColor: 0, + backgroundColor: ORANGE_400, + }); + } + if (route.__vd_match) { + tags.push({ + label: 'matches', + textColor: 0, + backgroundColor: PINK_500, + }); + } + if (route.__vd_exactActive) { + tags.push({ + label: 'exact', + textColor: 0, + backgroundColor: LIME_500, + }); + } + if (route.__vd_active) { + tags.push({ + label: 'active', + textColor: 0, + backgroundColor: BLUE_600, + }); + } + if (record.redirect) { + tags.push({ + label: typeof record.redirect === 'string' + ? `redirect: ${record.redirect}` + : 'redirects', + textColor: 0xffffff, + backgroundColor: DARK, + }); + } + // add an id to be able to select it. Using the `path` is not possible because + // empty path children would collide with their parents + let id = record.__vd_id; + if (id == null) { + id = String(routeRecordId++); + record.__vd_id = id; + } + return { + id, + label: record.path, + tags, + children: route.children.map(formatRouteRecordForInspector), + }; + } + // incremental id for route records and inspector state + let routeRecordId = 0; + const EXTRACT_REGEXP_RE = /^\/(.*)\/([a-z]*)$/; + function markRouteRecordActive(route, currentRoute) { + // no route will be active if matched is empty + // reset the matching state + const isExactActive = currentRoute.matched.length && + isSameRouteRecord(currentRoute.matched[currentRoute.matched.length - 1], route.record); + route.__vd_exactActive = route.__vd_active = isExactActive; + if (!isExactActive) { + route.__vd_active = currentRoute.matched.some(match => isSameRouteRecord(match, route.record)); + } + route.children.forEach(childRoute => markRouteRecordActive(childRoute, currentRoute)); + } + function resetMatchStateOnRouteRecord(route) { + route.__vd_match = false; + route.children.forEach(resetMatchStateOnRouteRecord); + } + function isRouteMatching(route, filter) { + const found = String(route.re).match(EXTRACT_REGEXP_RE); + route.__vd_match = false; + if (!found || found.length < 3) { + return false; + } + // use a regexp without $ at the end to match nested routes better + const nonEndingRE = new RegExp(found[1].replace(/\$$/, ''), found[2]); + if (nonEndingRE.test(filter)) { + // mark children as matches + route.children.forEach(child => isRouteMatching(child, filter)); + // exception case: `/` + if (route.record.path !== '/' || filter === '/') { + route.__vd_match = route.re.test(filter); + return true; + } + // hide the / route + return false; + } + const path = route.record.path.toLowerCase(); + const decodedPath = decode(path); + // also allow partial matching on the path + if (!filter.startsWith('/') && + (decodedPath.includes(filter) || path.includes(filter))) + return true; + if (decodedPath.startsWith(filter) || path.startsWith(filter)) + return true; + if (route.record.name && String(route.record.name).includes(filter)) + return true; + return route.children.some(child => isRouteMatching(child, filter)); + } + function omit(obj, keys) { + const ret = {}; + for (const key in obj) { + if (!keys.includes(key)) { + // @ts-expect-error + ret[key] = obj[key]; + } + } + return ret; + } + + /** + * Creates a Router instance that can be used by a Vue app. + * + * @param options - {@link RouterOptions} + */ + function createRouter(options) { + const matcher = createRouterMatcher(options.routes, options); + const parseQuery$1 = options.parseQuery || parseQuery; + const stringifyQuery$1 = options.stringifyQuery || stringifyQuery; + const routerHistory = options.history; + if (!routerHistory) + throw new Error('Provide the "history" option when calling "createRouter()":' + + ' https://next.router.vuejs.org/api/#history.'); + const beforeGuards = useCallbacks(); + const beforeResolveGuards = useCallbacks(); + const afterGuards = useCallbacks(); + const currentRoute = vue.shallowRef(START_LOCATION_NORMALIZED); + let pendingLocation = START_LOCATION_NORMALIZED; + // leave the scrollRestoration if no scrollBehavior is provided + if (isBrowser && options.scrollBehavior && 'scrollRestoration' in history) { + history.scrollRestoration = 'manual'; + } + const normalizeParams = applyToParams.bind(null, paramValue => '' + paramValue); + const encodeParams = applyToParams.bind(null, encodeParam); + const decodeParams = + // @ts-expect-error: intentionally avoid the type check + applyToParams.bind(null, decode); + function addRoute(parentOrRoute, route) { + let parent; + let record; + if (isRouteName(parentOrRoute)) { + parent = matcher.getRecordMatcher(parentOrRoute); + record = route; + } + else { + record = parentOrRoute; + } + return matcher.addRoute(record, parent); + } + function removeRoute(name) { + const recordMatcher = matcher.getRecordMatcher(name); + if (recordMatcher) { + matcher.removeRoute(recordMatcher); + } + else { + warn(`Cannot remove non-existent route "${String(name)}"`); + } + } + function getRoutes() { + return matcher.getRoutes().map(routeMatcher => routeMatcher.record); + } + function hasRoute(name) { + return !!matcher.getRecordMatcher(name); + } + function resolve(rawLocation, currentLocation) { + // const objectLocation = routerLocationAsObject(rawLocation) + // we create a copy to modify it later + currentLocation = assign({}, currentLocation || currentRoute.value); + if (typeof rawLocation === 'string') { + const locationNormalized = parseURL(parseQuery$1, rawLocation, currentLocation.path); + const matchedRoute = matcher.resolve({ path: locationNormalized.path }, currentLocation); + const href = routerHistory.createHref(locationNormalized.fullPath); + { + if (href.startsWith('//')) + warn(`Location "${rawLocation}" resolved to "${href}". A resolved location cannot start with multiple slashes.`); + else if (!matchedRoute.matched.length) { + warn(`No match found for location with path "${rawLocation}"`); + } + } + // locationNormalized is always a new object + return assign(locationNormalized, matchedRoute, { + params: decodeParams(matchedRoute.params), + hash: decode(locationNormalized.hash), + redirectedFrom: undefined, + href, + }); + } + let matcherLocation; + // path could be relative in object as well + if ('path' in rawLocation) { + if ('params' in rawLocation && + !('name' in rawLocation) && + // @ts-expect-error: the type is never + Object.keys(rawLocation.params).length) { + warn(`Path "${rawLocation.path}" was passed with params but they will be ignored. Use a named route alongside params instead.`); + } + matcherLocation = assign({}, rawLocation, { + path: parseURL(parseQuery$1, rawLocation.path, currentLocation.path).path, + }); + } + else { + // remove any nullish param + const targetParams = assign({}, rawLocation.params); + for (const key in targetParams) { + if (targetParams[key] == null) { + delete targetParams[key]; + } + } + // pass encoded values to the matcher, so it can produce encoded path and fullPath + matcherLocation = assign({}, rawLocation, { + params: encodeParams(targetParams), + }); + // current location params are decoded, we need to encode them in case the + // matcher merges the params + currentLocation.params = encodeParams(currentLocation.params); + } + const matchedRoute = matcher.resolve(matcherLocation, currentLocation); + const hash = rawLocation.hash || ''; + if (hash && !hash.startsWith('#')) { + warn(`A \`hash\` should always start with the character "#". Replace "${hash}" with "#${hash}".`); + } + // the matcher might have merged current location params, so + // we need to run the decoding again + matchedRoute.params = normalizeParams(decodeParams(matchedRoute.params)); + const fullPath = stringifyURL(stringifyQuery$1, assign({}, rawLocation, { + hash: encodeHash(hash), + path: matchedRoute.path, + })); + const href = routerHistory.createHref(fullPath); + { + if (href.startsWith('//')) { + warn(`Location "${rawLocation}" resolved to "${href}". A resolved location cannot start with multiple slashes.`); + } + else if (!matchedRoute.matched.length) { + warn(`No match found for location with path "${'path' in rawLocation ? rawLocation.path : rawLocation}"`); + } + } + return assign({ + fullPath, + // keep the hash encoded so fullPath is effectively path + encodedQuery + + // hash + hash, + query: + // if the user is using a custom query lib like qs, we might have + // nested objects, so we keep the query as is, meaning it can contain + // numbers at `$route.query`, but at the point, the user will have to + // use their own type anyway. + // https://github.com/vuejs/router/issues/328#issuecomment-649481567 + stringifyQuery$1 === stringifyQuery + ? normalizeQuery(rawLocation.query) + : (rawLocation.query || {}), + }, matchedRoute, { + redirectedFrom: undefined, + href, + }); + } + function locationAsObject(to) { + return typeof to === 'string' + ? parseURL(parseQuery$1, to, currentRoute.value.path) + : assign({}, to); + } + function checkCanceledNavigation(to, from) { + if (pendingLocation !== to) { + return createRouterError(8 /* ErrorTypes.NAVIGATION_CANCELLED */, { + from, + to, + }); + } + } + function push(to) { + return pushWithRedirect(to); + } + function replace(to) { + return push(assign(locationAsObject(to), { replace: true })); + } + function handleRedirectRecord(to) { + const lastMatched = to.matched[to.matched.length - 1]; + if (lastMatched && lastMatched.redirect) { + const { redirect } = lastMatched; + let newTargetLocation = typeof redirect === 'function' ? redirect(to) : redirect; + if (typeof newTargetLocation === 'string') { + newTargetLocation = + newTargetLocation.includes('?') || newTargetLocation.includes('#') + ? (newTargetLocation = locationAsObject(newTargetLocation)) + : // force empty params + { path: newTargetLocation }; + // @ts-expect-error: force empty params when a string is passed to let + // the router parse them again + newTargetLocation.params = {}; + } + if (!('path' in newTargetLocation) && + !('name' in newTargetLocation)) { + warn(`Invalid redirect found:\n${JSON.stringify(newTargetLocation, null, 2)}\n when navigating to "${to.fullPath}". A redirect must contain a name or path. This will break in production.`); + throw new Error('Invalid redirect'); + } + return assign({ + query: to.query, + hash: to.hash, + // avoid transferring params if the redirect has a path + params: 'path' in newTargetLocation ? {} : to.params, + }, newTargetLocation); + } + } + function pushWithRedirect(to, redirectedFrom) { + const targetLocation = (pendingLocation = resolve(to)); + const from = currentRoute.value; + const data = to.state; + const force = to.force; + // to could be a string where `replace` is a function + const replace = to.replace === true; + const shouldRedirect = handleRedirectRecord(targetLocation); + if (shouldRedirect) + return pushWithRedirect(assign(locationAsObject(shouldRedirect), { + state: typeof shouldRedirect === 'object' + ? assign({}, data, shouldRedirect.state) + : data, + force, + replace, + }), + // keep original redirectedFrom if it exists + redirectedFrom || targetLocation); + // if it was a redirect we already called `pushWithRedirect` above + const toLocation = targetLocation; + toLocation.redirectedFrom = redirectedFrom; + let failure; + if (!force && isSameRouteLocation(stringifyQuery$1, from, targetLocation)) { + failure = createRouterError(16 /* ErrorTypes.NAVIGATION_DUPLICATED */, { to: toLocation, from }); + // trigger scroll to allow scrolling to the same anchor + handleScroll(from, from, + // this is a push, the only way for it to be triggered from a + // history.listen is with a redirect, which makes it become a push + true, + // This cannot be the first navigation because the initial location + // cannot be manually navigated to + false); + } + return (failure ? Promise.resolve(failure) : navigate(toLocation, from)) + .catch((error) => isNavigationFailure(error) + ? // navigation redirects still mark the router as ready + isNavigationFailure(error, 2 /* ErrorTypes.NAVIGATION_GUARD_REDIRECT */) + ? error + : markAsReady(error) // also returns the error + : // reject any unknown error + triggerError(error, toLocation, from)) + .then((failure) => { + if (failure) { + if (isNavigationFailure(failure, 2 /* ErrorTypes.NAVIGATION_GUARD_REDIRECT */)) { + if (// we are redirecting to the same location we were already at + isSameRouteLocation(stringifyQuery$1, resolve(failure.to), toLocation) && + // and we have done it a couple of times + redirectedFrom && + // @ts-expect-error: added only in dev + (redirectedFrom._count = redirectedFrom._count + ? // @ts-expect-error + redirectedFrom._count + 1 + : 1) > 30) { + warn(`Detected a possibly infinite redirection in a navigation guard when going from "${from.fullPath}" to "${toLocation.fullPath}". Aborting to avoid a Stack Overflow.\n Are you always returning a new location within a navigation guard? That would lead to this error. Only return when redirecting or aborting, that should fix this. This might break in production if not fixed.`); + return Promise.reject(new Error('Infinite redirect in navigation guard')); + } + return pushWithRedirect( + // keep options + assign({ + // preserve an existing replacement but allow the redirect to override it + replace, + }, locationAsObject(failure.to), { + state: typeof failure.to === 'object' + ? assign({}, data, failure.to.state) + : data, + force, + }), + // preserve the original redirectedFrom if any + redirectedFrom || toLocation); + } + } + else { + // if we fail we don't finalize the navigation + failure = finalizeNavigation(toLocation, from, true, replace, data); + } + triggerAfterEach(toLocation, from, failure); + return failure; + }); + } + /** + * Helper to reject and skip all navigation guards if a new navigation happened + * @param to + * @param from + */ + function checkCanceledNavigationAndReject(to, from) { + const error = checkCanceledNavigation(to, from); + return error ? Promise.reject(error) : Promise.resolve(); + } + function runWithContext(fn) { + const app = installedApps.values().next().value; + // support Vue < 3.3 + return app && typeof app.runWithContext === 'function' + ? app.runWithContext(fn) + : fn(); + } + // TODO: refactor the whole before guards by internally using router.beforeEach + function navigate(to, from) { + let guards; + const [leavingRecords, updatingRecords, enteringRecords] = extractChangingRecords(to, from); + // all components here have been resolved once because we are leaving + guards = extractComponentsGuards(leavingRecords.reverse(), 'beforeRouteLeave', to, from); + // leavingRecords is already reversed + for (const record of leavingRecords) { + record.leaveGuards.forEach(guard => { + guards.push(guardToPromiseFn(guard, to, from)); + }); + } + const canceledNavigationCheck = checkCanceledNavigationAndReject.bind(null, to, from); + guards.push(canceledNavigationCheck); + // run the queue of per route beforeRouteLeave guards + return (runGuardQueue(guards) + .then(() => { + // check global guards beforeEach + guards = []; + for (const guard of beforeGuards.list()) { + guards.push(guardToPromiseFn(guard, to, from)); + } + guards.push(canceledNavigationCheck); + return runGuardQueue(guards); + }) + .then(() => { + // check in components beforeRouteUpdate + guards = extractComponentsGuards(updatingRecords, 'beforeRouteUpdate', to, from); + for (const record of updatingRecords) { + record.updateGuards.forEach(guard => { + guards.push(guardToPromiseFn(guard, to, from)); + }); + } + guards.push(canceledNavigationCheck); + // run the queue of per route beforeEnter guards + return runGuardQueue(guards); + }) + .then(() => { + // check the route beforeEnter + guards = []; + for (const record of to.matched) { + // do not trigger beforeEnter on reused views + if (record.beforeEnter && !from.matched.includes(record)) { + if (isArray(record.beforeEnter)) { + for (const beforeEnter of record.beforeEnter) + guards.push(guardToPromiseFn(beforeEnter, to, from)); + } + else { + guards.push(guardToPromiseFn(record.beforeEnter, to, from)); + } + } + } + guards.push(canceledNavigationCheck); + // run the queue of per route beforeEnter guards + return runGuardQueue(guards); + }) + .then(() => { + // NOTE: at this point to.matched is normalized and does not contain any () => Promise + // clear existing enterCallbacks, these are added by extractComponentsGuards + to.matched.forEach(record => (record.enterCallbacks = {})); + // check in-component beforeRouteEnter + guards = extractComponentsGuards(enteringRecords, 'beforeRouteEnter', to, from); + guards.push(canceledNavigationCheck); + // run the queue of per route beforeEnter guards + return runGuardQueue(guards); + }) + .then(() => { + // check global guards beforeResolve + guards = []; + for (const guard of beforeResolveGuards.list()) { + guards.push(guardToPromiseFn(guard, to, from)); + } + guards.push(canceledNavigationCheck); + return runGuardQueue(guards); + }) + // catch any navigation canceled + .catch(err => isNavigationFailure(err, 8 /* ErrorTypes.NAVIGATION_CANCELLED */) + ? err + : Promise.reject(err))); + } + function triggerAfterEach(to, from, failure) { + // navigation is confirmed, call afterGuards + // TODO: wrap with error handlers + for (const guard of afterGuards.list()) { + runWithContext(() => guard(to, from, failure)); + } + } + /** + * - Cleans up any navigation guards + * - Changes the url if necessary + * - Calls the scrollBehavior + */ + function finalizeNavigation(toLocation, from, isPush, replace, data) { + // a more recent navigation took place + const error = checkCanceledNavigation(toLocation, from); + if (error) + return error; + // only consider as push if it's not the first navigation + const isFirstNavigation = from === START_LOCATION_NORMALIZED; + const state = !isBrowser ? {} : history.state; + // change URL only if the user did a push/replace and if it's not the initial navigation because + // it's just reflecting the url + if (isPush) { + // on the initial navigation, we want to reuse the scroll position from + // history state if it exists + if (replace || isFirstNavigation) + routerHistory.replace(toLocation.fullPath, assign({ + scroll: isFirstNavigation && state && state.scroll, + }, data)); + else + routerHistory.push(toLocation.fullPath, data); + } + // accept current navigation + currentRoute.value = toLocation; + handleScroll(toLocation, from, isPush, isFirstNavigation); + markAsReady(); + } + let removeHistoryListener; + // attach listener to history to trigger navigations + function setupListeners() { + // avoid setting up listeners twice due to an invalid first navigation + if (removeHistoryListener) + return; + removeHistoryListener = routerHistory.listen((to, _from, info) => { + if (!router.listening) + return; + // cannot be a redirect route because it was in history + const toLocation = resolve(to); + // due to dynamic routing, and to hash history with manual navigation + // (manually changing the url or calling history.hash = '#/somewhere'), + // there could be a redirect record in history + const shouldRedirect = handleRedirectRecord(toLocation); + if (shouldRedirect) { + pushWithRedirect(assign(shouldRedirect, { replace: true }), toLocation).catch(noop); + return; + } + pendingLocation = toLocation; + const from = currentRoute.value; + // TODO: should be moved to web history? + if (isBrowser) { + saveScrollPosition(getScrollKey(from.fullPath, info.delta), computeScrollPosition()); + } + navigate(toLocation, from) + .catch((error) => { + if (isNavigationFailure(error, 4 /* ErrorTypes.NAVIGATION_ABORTED */ | 8 /* ErrorTypes.NAVIGATION_CANCELLED */)) { + return error; + } + if (isNavigationFailure(error, 2 /* ErrorTypes.NAVIGATION_GUARD_REDIRECT */)) { + // Here we could call if (info.delta) routerHistory.go(-info.delta, + // false) but this is bug prone as we have no way to wait the + // navigation to be finished before calling pushWithRedirect. Using + // a setTimeout of 16ms seems to work but there is no guarantee for + // it to work on every browser. So instead we do not restore the + // history entry and trigger a new navigation as requested by the + // navigation guard. + // the error is already handled by router.push we just want to avoid + // logging the error + pushWithRedirect(error.to, toLocation + // avoid an uncaught rejection, let push call triggerError + ) + .then(failure => { + // manual change in hash history #916 ending up in the URL not + // changing, but it was changed by the manual url change, so we + // need to manually change it ourselves + if (isNavigationFailure(failure, 4 /* ErrorTypes.NAVIGATION_ABORTED */ | + 16 /* ErrorTypes.NAVIGATION_DUPLICATED */) && + !info.delta && + info.type === NavigationType.pop) { + routerHistory.go(-1, false); + } + }) + .catch(noop); + // avoid the then branch + return Promise.reject(); + } + // do not restore history on unknown direction + if (info.delta) { + routerHistory.go(-info.delta, false); + } + // unrecognized error, transfer to the global handler + return triggerError(error, toLocation, from); + }) + .then((failure) => { + failure = + failure || + finalizeNavigation( + // after navigation, all matched components are resolved + toLocation, from, false); + // revert the navigation + if (failure) { + if (info.delta && + // a new navigation has been triggered, so we do not want to revert, that will change the current history + // entry while a different route is displayed + !isNavigationFailure(failure, 8 /* ErrorTypes.NAVIGATION_CANCELLED */)) { + routerHistory.go(-info.delta, false); + } + else if (info.type === NavigationType.pop && + isNavigationFailure(failure, 4 /* ErrorTypes.NAVIGATION_ABORTED */ | 16 /* ErrorTypes.NAVIGATION_DUPLICATED */)) { + // manual change in hash history #916 + // it's like a push but lacks the information of the direction + routerHistory.go(-1, false); + } + } + triggerAfterEach(toLocation, from, failure); + }) + .catch(noop); + }); + } + // Initialization and Errors + let readyHandlers = useCallbacks(); + let errorHandlers = useCallbacks(); + let ready; + /** + * Trigger errorHandlers added via onError and throws the error as well + * + * @param error - error to throw + * @param to - location we were navigating to when the error happened + * @param from - location we were navigating from when the error happened + * @returns the error as a rejected promise + */ + function triggerError(error, to, from) { + markAsReady(error); + const list = errorHandlers.list(); + if (list.length) { + list.forEach(handler => handler(error, to, from)); + } + else { + { + warn('uncaught error during route navigation:'); + } + console.error(error); + } + return Promise.reject(error); + } + function isReady() { + if (ready && currentRoute.value !== START_LOCATION_NORMALIZED) + return Promise.resolve(); + return new Promise((resolve, reject) => { + readyHandlers.add([resolve, reject]); + }); + } + function markAsReady(err) { + if (!ready) { + // still not ready if an error happened + ready = !err; + setupListeners(); + readyHandlers + .list() + .forEach(([resolve, reject]) => (err ? reject(err) : resolve())); + readyHandlers.reset(); + } + return err; + } + // Scroll behavior + function handleScroll(to, from, isPush, isFirstNavigation) { + const { scrollBehavior } = options; + if (!isBrowser || !scrollBehavior) + return Promise.resolve(); + const scrollPosition = (!isPush && getSavedScrollPosition(getScrollKey(to.fullPath, 0))) || + ((isFirstNavigation || !isPush) && + history.state && + history.state.scroll) || + null; + return vue.nextTick() + .then(() => scrollBehavior(to, from, scrollPosition)) + .then(position => position && scrollToPosition(position)) + .catch(err => triggerError(err, to, from)); + } + const go = (delta) => routerHistory.go(delta); + let started; + const installedApps = new Set(); + const router = { + currentRoute, + listening: true, + addRoute, + removeRoute, + hasRoute, + getRoutes, + resolve, + options, + push, + replace, + go, + back: () => go(-1), + forward: () => go(1), + beforeEach: beforeGuards.add, + beforeResolve: beforeResolveGuards.add, + afterEach: afterGuards.add, + onError: errorHandlers.add, + isReady, + install(app) { + const router = this; + app.component('RouterLink', RouterLink); + app.component('RouterView', RouterView); + app.config.globalProperties.$router = router; + Object.defineProperty(app.config.globalProperties, '$route', { + enumerable: true, + get: () => vue.unref(currentRoute), + }); + // this initial navigation is only necessary on client, on server it doesn't + // make sense because it will create an extra unnecessary navigation and could + // lead to problems + if (isBrowser && + // used for the initial navigation client side to avoid pushing + // multiple times when the router is used in multiple apps + !started && + currentRoute.value === START_LOCATION_NORMALIZED) { + // see above + started = true; + push(routerHistory.location).catch(err => { + warn('Unexpected error when starting the router:', err); + }); + } + const reactiveRoute = {}; + for (const key in START_LOCATION_NORMALIZED) { + // @ts-expect-error: the key matches + reactiveRoute[key] = vue.computed(() => currentRoute.value[key]); + } + app.provide(routerKey, router); + app.provide(routeLocationKey, vue.reactive(reactiveRoute)); + app.provide(routerViewLocationKey, currentRoute); + const unmountApp = app.unmount; + installedApps.add(app); + app.unmount = function () { + installedApps.delete(app); + // the router is not attached to an app anymore + if (installedApps.size < 1) { + // invalidate the current navigation + pendingLocation = START_LOCATION_NORMALIZED; + removeHistoryListener && removeHistoryListener(); + removeHistoryListener = null; + currentRoute.value = START_LOCATION_NORMALIZED; + started = false; + ready = false; + } + unmountApp(); + }; + // TODO: this probably needs to be updated so it can be used by vue-termui + if (isBrowser) { + addDevtools(app, router, matcher); + } + }, + }; + // TODO: type this as NavigationGuardReturn or similar instead of any + function runGuardQueue(guards) { + return guards.reduce((promise, guard) => promise.then(() => runWithContext(guard)), Promise.resolve()); + } + return router; + } + function extractChangingRecords(to, from) { + const leavingRecords = []; + const updatingRecords = []; + const enteringRecords = []; + const len = Math.max(from.matched.length, to.matched.length); + for (let i = 0; i < len; i++) { + const recordFrom = from.matched[i]; + if (recordFrom) { + if (to.matched.find(record => isSameRouteRecord(record, recordFrom))) + updatingRecords.push(recordFrom); + else + leavingRecords.push(recordFrom); + } + const recordTo = to.matched[i]; + if (recordTo) { + // the type doesn't matter because we are comparing per reference + if (!from.matched.find(record => isSameRouteRecord(record, recordTo))) { + enteringRecords.push(recordTo); + } + } + } + return [leavingRecords, updatingRecords, enteringRecords]; + } + + /** + * Returns the router instance. Equivalent to using `$router` inside + * templates. + */ + function useRouter() { + return vue.inject(routerKey); + } + /** + * Returns the current route location. Equivalent to using `$route` inside + * templates. + */ + function useRoute() { + return vue.inject(routeLocationKey); + } + + exports.RouterLink = RouterLink; + exports.RouterView = RouterView; + exports.START_LOCATION = START_LOCATION_NORMALIZED; + exports.createMemoryHistory = createMemoryHistory; + exports.createRouter = createRouter; + exports.createRouterMatcher = createRouterMatcher; + exports.createWebHashHistory = createWebHashHistory; + exports.createWebHistory = createWebHistory; + exports.isNavigationFailure = isNavigationFailure; + exports.loadRouteLocation = loadRouteLocation; + exports.matchedRouteKey = matchedRouteKey; + exports.onBeforeRouteLeave = onBeforeRouteLeave; + exports.onBeforeRouteUpdate = onBeforeRouteUpdate; + exports.parseQuery = parseQuery; + exports.routeLocationKey = routeLocationKey; + exports.routerKey = routerKey; + exports.routerViewLocationKey = routerViewLocationKey; + exports.stringifyQuery = stringifyQuery; + exports.useLink = useLink; + exports.useRoute = useRoute; + exports.useRouter = useRouter; + exports.viewDepthKey = viewDepthKey; + + return exports; + + })({}, Vue); + \ No newline at end of file