From 14eaaa29b6cd63f9b8e447f961a9635924e568c7 Mon Sep 17 00:00:00 2001 From: Hieu Nv Date: Wed, 25 Sep 2019 11:09:20 +0700 Subject: [PATCH] add main files of module --- Api/Data/MenuInterface.php | 150 + Api/Data/MenuSearchResultsInterface.php | 47 + Api/Data/NodeInterface.php | 253 + Api/Data/NodeTypeInterface.php | 46 + Api/MenuRepositoryInterface.php | 82 + Api/NodeRepositoryInterface.php | 80 + Api/NodeTypeInterface.php | 62 + Block/Menu.php | 514 + Block/Menu/Edit.php | 56 + Block/Menu/Edit/Form.php | 66 + Block/Menu/Edit/Tab/Main.php | 157 + Block/Menu/Edit/Tab/Nodes.php | 194 + Block/Menu/Edit/Tabs.php | 40 + Block/NodeType/AbstractNode.php | 168 + Block/NodeType/Category.php | 234 + Block/NodeType/CmsBlock.php | 140 + Block/NodeType/CmsPage.php | 205 + Block/NodeType/CustomUrl.php | 140 + Block/NodeType/Product.php | 287 + Block/NodeType/Wrapper.php | 121 + Controller/Adminhtml/Menu/Create.php | 55 + Controller/Adminhtml/Menu/Delete.php | 134 + Controller/Adminhtml/Menu/Edit.php | 99 + Controller/Adminhtml/Menu/Index.php | 67 + Controller/Adminhtml/Menu/Save.php | 292 + Model/Menu.php | 208 + Model/Menu/Node.php | 261 + Model/Menu/NodeRepository.php | 201 + Model/MenuRepository.php | 178 + Model/NodeType/AbstractNode.php | 128 + Model/NodeType/Category.php | 161 + Model/NodeType/CmsBlock.php | 97 + Model/NodeType/CmsPage.php | 94 + Model/NodeType/CustomUrl.php | 30 + Model/NodeType/Product.php | 96 + Model/NodeType/Wrapper.php | 30 + Model/NodeTypeProvider.php | 94 + Model/ResourceModel/Menu.php | 41 + Model/ResourceModel/Menu/Collection.php | 44 + Model/ResourceModel/Menu/Node.php | 41 + Model/ResourceModel/Menu/Node/Collection.php | 44 + Model/ResourceModel/NodeType/AbstractNode.php | 118 + Model/ResourceModel/NodeType/Category.php | 115 + Model/ResourceModel/NodeType/CmsBlock.php | 83 + Model/ResourceModel/NodeType/CmsPage.php | 166 + Model/ResourceModel/NodeType/Product.php | 145 + Model/TemplateResolver.php | 89 + README.md | 0 Setup/InstallData.php | 37 + Setup/InstallSchema.php | 184 + Setup/UpgradeSchema.php | 203 + .../Listing/Column/MenuList/MenuActions.php | 57 + .../EcomteckMenu/Menu/ListProvider.php | 53 + composer.json | 0 etc/acl.xml | 36 + etc/adminhtml/menu.xml | 27 + etc/adminhtml/routes.xml | 30 + etc/di.xml | 43 + etc/module.xml | 24 +- etc/webapi.xml | 43 + registration.php | 22 +- .../adminhtml/layout/megamenu_menu_create.xml | 28 + view/adminhtml/layout/megamenu_menu_edit.xml | 46 + view/adminhtml/layout/megamenu_menu_index.xml | 31 + view/adminhtml/requirejs-config.js | 11 + view/adminhtml/templates/menu/nodes.phtml | 81 + .../ui_component/megamenu_menu_list.xml | 122 + view/adminhtml/web/css/source/_module.less | 5 + .../web/css/source/blocks/_nested-list.less | 15 + .../web/css/source/blocks/_panel.less | 162 + .../css/source/blocks/_selected-option.less | 36 + .../adminhtml/web/css/source/blocks/_var.less | 1 + .../web/css/source/blocks/_vddl-base.less | 31 + view/adminhtml/web/js/lib/require-vuejs.js | 267 + view/adminhtml/web/js/lib/vddl.js | 498 + view/adminhtml/web/js/lib/vue-select.js | 2 + view/adminhtml/web/js/lib/vue.js | 10798 ++++++++++++++++ view/adminhtml/web/js/lib/vue.min.js | 6 + view/adminhtml/web/vue/app.vue | 103 + .../web/vue/field-type/autocomplete.vue | 63 + view/adminhtml/web/vue/field-type/simple.vue | 40 + view/adminhtml/web/vue/menu-type.vue | 92 + view/adminhtml/web/vue/menu-type/category.vue | 20 + .../adminhtml/web/vue/menu-type/cms-block.vue | 20 + view/adminhtml/web/vue/menu-type/cms-page.vue | 20 + .../web/vue/menu-type/custom-url.vue | 38 + view/adminhtml/web/vue/menu-type/product.vue | 19 + view/adminhtml/web/vue/nested-list.vue | 165 + view/frontend/templates/menu.phtml | 30 + .../templates/menu/node_type/category.phtml | 29 + .../templates/menu/node_type/cms_block.phtml | 2 + .../templates/menu/node_type/cms_page.phtml | 29 + .../templates/menu/node_type/custom_url.phtml | 23 + .../templates/menu/node_type/product.phtml | 28 + .../templates/menu/node_type/wrapper.phtml | 16 + view/frontend/templates/menu/sub_menu.phtml | 58 + 96 files changed, 19845 insertions(+), 2 deletions(-) create mode 100755 Api/Data/MenuInterface.php create mode 100755 Api/Data/MenuSearchResultsInterface.php create mode 100755 Api/Data/NodeInterface.php create mode 100755 Api/Data/NodeTypeInterface.php create mode 100755 Api/MenuRepositoryInterface.php create mode 100755 Api/NodeRepositoryInterface.php create mode 100755 Api/NodeTypeInterface.php create mode 100755 Block/Menu.php create mode 100755 Block/Menu/Edit.php create mode 100755 Block/Menu/Edit/Form.php create mode 100755 Block/Menu/Edit/Tab/Main.php create mode 100755 Block/Menu/Edit/Tab/Nodes.php create mode 100755 Block/Menu/Edit/Tabs.php create mode 100755 Block/NodeType/AbstractNode.php create mode 100755 Block/NodeType/Category.php create mode 100755 Block/NodeType/CmsBlock.php create mode 100755 Block/NodeType/CmsPage.php create mode 100755 Block/NodeType/CustomUrl.php create mode 100755 Block/NodeType/Product.php create mode 100755 Block/NodeType/Wrapper.php create mode 100755 Controller/Adminhtml/Menu/Create.php create mode 100755 Controller/Adminhtml/Menu/Delete.php create mode 100755 Controller/Adminhtml/Menu/Edit.php create mode 100755 Controller/Adminhtml/Menu/Index.php create mode 100755 Controller/Adminhtml/Menu/Save.php create mode 100755 Model/Menu.php create mode 100755 Model/Menu/Node.php create mode 100755 Model/Menu/NodeRepository.php create mode 100755 Model/MenuRepository.php create mode 100755 Model/NodeType/AbstractNode.php create mode 100755 Model/NodeType/Category.php create mode 100755 Model/NodeType/CmsBlock.php create mode 100755 Model/NodeType/CmsPage.php create mode 100755 Model/NodeType/CustomUrl.php create mode 100755 Model/NodeType/Product.php create mode 100755 Model/NodeType/Wrapper.php create mode 100755 Model/NodeTypeProvider.php create mode 100755 Model/ResourceModel/Menu.php create mode 100755 Model/ResourceModel/Menu/Collection.php create mode 100755 Model/ResourceModel/Menu/Node.php create mode 100755 Model/ResourceModel/Menu/Node/Collection.php create mode 100755 Model/ResourceModel/NodeType/AbstractNode.php create mode 100755 Model/ResourceModel/NodeType/Category.php create mode 100755 Model/ResourceModel/NodeType/CmsBlock.php create mode 100755 Model/ResourceModel/NodeType/CmsPage.php create mode 100755 Model/ResourceModel/NodeType/Product.php create mode 100755 Model/TemplateResolver.php mode change 100644 => 100755 README.md create mode 100755 Setup/InstallData.php create mode 100755 Setup/InstallSchema.php create mode 100755 Setup/UpgradeSchema.php create mode 100755 Ui/Component/Listing/Column/MenuList/MenuActions.php create mode 100755 Ui/Component/Listing/DataProviders/EcomteckMenu/Menu/ListProvider.php mode change 100644 => 100755 composer.json create mode 100755 etc/acl.xml create mode 100755 etc/adminhtml/menu.xml create mode 100755 etc/adminhtml/routes.xml create mode 100755 etc/di.xml mode change 100644 => 100755 etc/module.xml create mode 100755 etc/webapi.xml mode change 100644 => 100755 registration.php create mode 100755 view/adminhtml/layout/megamenu_menu_create.xml create mode 100755 view/adminhtml/layout/megamenu_menu_edit.xml create mode 100755 view/adminhtml/layout/megamenu_menu_index.xml create mode 100755 view/adminhtml/requirejs-config.js create mode 100755 view/adminhtml/templates/menu/nodes.phtml create mode 100755 view/adminhtml/ui_component/megamenu_menu_list.xml create mode 100755 view/adminhtml/web/css/source/_module.less create mode 100755 view/adminhtml/web/css/source/blocks/_nested-list.less create mode 100755 view/adminhtml/web/css/source/blocks/_panel.less create mode 100755 view/adminhtml/web/css/source/blocks/_selected-option.less create mode 100755 view/adminhtml/web/css/source/blocks/_var.less create mode 100755 view/adminhtml/web/css/source/blocks/_vddl-base.less create mode 100755 view/adminhtml/web/js/lib/require-vuejs.js create mode 100755 view/adminhtml/web/js/lib/vddl.js create mode 100755 view/adminhtml/web/js/lib/vue-select.js create mode 100755 view/adminhtml/web/js/lib/vue.js create mode 100755 view/adminhtml/web/js/lib/vue.min.js create mode 100755 view/adminhtml/web/vue/app.vue create mode 100755 view/adminhtml/web/vue/field-type/autocomplete.vue create mode 100755 view/adminhtml/web/vue/field-type/simple.vue create mode 100755 view/adminhtml/web/vue/menu-type.vue create mode 100755 view/adminhtml/web/vue/menu-type/category.vue create mode 100755 view/adminhtml/web/vue/menu-type/cms-block.vue create mode 100755 view/adminhtml/web/vue/menu-type/cms-page.vue create mode 100755 view/adminhtml/web/vue/menu-type/custom-url.vue create mode 100755 view/adminhtml/web/vue/menu-type/product.vue create mode 100755 view/adminhtml/web/vue/nested-list.vue create mode 100755 view/frontend/templates/menu.phtml create mode 100755 view/frontend/templates/menu/node_type/category.phtml create mode 100755 view/frontend/templates/menu/node_type/cms_block.phtml create mode 100755 view/frontend/templates/menu/node_type/cms_page.phtml create mode 100755 view/frontend/templates/menu/node_type/custom_url.phtml create mode 100755 view/frontend/templates/menu/node_type/product.phtml create mode 100755 view/frontend/templates/menu/node_type/wrapper.phtml create mode 100755 view/frontend/templates/menu/sub_menu.phtml diff --git a/Api/Data/MenuInterface.php b/Api/Data/MenuInterface.php new file mode 100755 index 0000000..2678fc8 --- /dev/null +++ b/Api/Data/MenuInterface.php @@ -0,0 +1,150 @@ +menuRepository = $menuRepository; + $this->nodeRepository = $nodeRepository; + $this->nodeTypeProvider = $nodeTypeProvider; + $this->searchCriteriaFactory = $searchCriteriaFactory; + $this->filterGroupBuilder = $filterGroupBuilder; + $this->eventManager = $eventManager; + $this->templateResolver = $templateResolver; + $this->submenuTemplate = $this->getMenuTemplate( + 'Ecomteck_Megamenu::menu/sub_menu.phtml' + ); + $this->setTemplate($this->getMenuTemplate($this->_template)); + } + + /** + * Return unique ID(s) for each object in system + * + * @return string[] + */ + public function getIdentities() + { + return [\Ecomteck\Megamenu\Model\Menu::CACHE_TAG, Block::CACHE_TAG]; + } + + protected function getCacheLifetime() + { + return 60*60*24*365; + } + + /** + * @return array|\Ecomteck\Megamenu\Model\Menu + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function loadMenu() + { + if (!$this->menu) { + $storeId = $this->_storeManager->getStore()->getId(); + $this->menu = $this->menuRepository->get($this->getData('menu'), $storeId); + } + return $this->menu; + } + + /** + * @return array|\Ecomteck\Megamenu\Model\Menu|null + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function getMenu() + { + $menu = $this->loadMenu(); + if (!$menu->getMenuId()) { + return null; + } + + return $menu; + } + + /** + * @return array + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function getCacheKeyInfo() + { + $info = [ + \Ecomteck\Megamenu\Model\Menu::CACHE_TAG, + 'menu_' . $this->loadMenu()->getId(), + 'store_' . $this->_storeManager->getStore()->getId(), + 'template_' . $this->getTemplate() + ]; + + $nodeCacheKeyInfo = $this->getNodeCacheKeyInfo(); + if ($nodeCacheKeyInfo) { + $info = array_merge($info, $nodeCacheKeyInfo); + } + + return $info; + } + + /** + * @return array + */ + private function getNodeCacheKeyInfo() + { + $info = []; + $nodeType = ''; + $request = $this->getRequest(); + + switch ($request->getRouteName()) { + case 'cms': + $nodeType = 'cms_page'; + break; + case 'catalog': + $nodeType = 'category'; + break; + } + + $transport = [ + 'node_type' => $nodeType, + 'request' => $request + ]; + + $transport = new DataObject($transport); + $this->eventManager->dispatch( + 'ecomteck_menu_cache_node_type', + ['transport' => $transport] + ); + + if ($transport->getNodeType()) { + $nodeType = $transport->getNodeType(); + } + + if ($nodeType) { + $info = $this->getNodeTypeProvider($nodeType)->getNodeCacheKeyInfo(); + } + + if ($this->getParentNode()) { + $info[] = 'parent_node_' . $this->getParentNode()->getNodeId(); + } + + return $info; + } + + /** + * @param int $level + * @param null $parent + * @return string + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function getMenuHtml($level = 0, $parent = null) + { + $nodes = $this->getNodes($level, $parent); + $html = ''; + $i = 0; + foreach ($nodes as $node) { + $children = $this->getMenuHtml($level + 1, $node); + $classes = [ + 'level' . $level, + $node->getClasses() ?: '', + ]; + if (!empty($children)) { + $classes[] = 'parent'; + } + if ($i == 0) { + $classes[] = 'first'; + } + if ($i == count($nodes) - 1) { + $classes[] = 'last'; + } + if ($level == 0) { + $classes[] = 'level-top'; + } + $html .= '
  • '; + $html .= $this->renderNode($node, $level); + if (!empty($children)) { + $html .= ''; + } + $html .= '
  • '; + ++$i; + } + return $html; + } + + /** + * @param string $nodeType + * @return bool + */ + public function isViewAllLinkAllowed($nodeType) + { + return $this->getNodeTypeProvider($nodeType)->isViewAllLinkAllowed(); + } + + /** + * @param $node + * @return mixed + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function renderViewAllLink($node) + { + return $this->getMenuNodeBlock($node) + ->setIsViewAllLink(true) + ->toHtml(); + } + + /** + * @param $node + * @return string + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function renderMenuNode($node) + { + return $this->getMenuNodeBlock($node)->toHtml(); + } + + /** + * @param array $nodes + * @param NodeRepositoryInterface $parentNode + * @param int $level + * @return string + */ + public function renderSubmenu($nodes, $parentNode, $level = 0) + { + return $nodes + ? $this->getSubmenuBlock($nodes, $parentNode, $level)->toHtml() + : ''; + } + + /** + * @param int $level + * @param null $parent + * @return array + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function getNodesTree($level = 0, $parent = null) + { + $nodesTree = []; + $nodes = $this->getNodes($level, $parent); + + foreach ($nodes as $node) { + $nodesTree[] = [ + 'node' => $node, + 'children' => $this->getNodesTree($level + 1, $node) + ]; + } + + return $nodesTree; + } + + /** + * @param string $nodeType + * @return \Ecomteck\Megamenu\Api\NodeTypeInterface + */ + public function getNodeTypeProvider($nodeType) + { + return $this->nodeTypeProvider->getProvider($nodeType); + } + + /** + * @param int $level + * @param null $parent + * @return array|mixed + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function getNodes($level = 0, $parent = null) + { + if (empty($this->nodes)) { + $this->fetchData(); + } + if (!isset($this->nodes[$level])) { + return []; + } + $parentId = $parent['node_id'] ?: 0; + if (!isset($this->nodes[$level][$parentId])) { + return []; + } + return $this->nodes[$level][$parentId]; + } + + /** + * Builds HTML tag attributes from an array of attributes data + * + * @param array $array + * @return string + */ + public function buildAttrFromArray(array $array) + { + $attributes = []; + + foreach ($array as $attribute => $data) { + if (is_array($data)) { + $data = implode(' ', $data); + } + + $attributes[] = $attribute . '="' . htmlspecialchars($data) . '"'; + } + + return $attributes ? ' ' . implode(' ', $attributes) : ''; + } + + /** + * @param string $defaultClass + * @return mixed|string + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function getMenuCssClass($defaultClass = '') + { + $menu = $this->getMenu(); + + if (is_null($menu)) { + return $defaultClass; + } + + return $menu->getCssClass(); + } + + /** + * @param $node + * @return \Ecomteck\Megamenu\Api\NodeTypeInterface + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function getMenuNodeBlock($node) + { + $nodeBlock = $this->getNodeTypeProvider($node->getType()); + + $level = $node->getLevel(); + $isRoot = 0 == $level; + $nodeBlock->setId($node->getNodeId()) + ->setTitle($node->getTitle()) + ->setLevel($level) + ->setIsRoot($isRoot) + ->setIsParent((bool) $node->getIsParent()) + ->setIsViewAllLink(false) + ->setContent($node->getContent()) + ->setNodeClasses($node->getClasses()) + ->setMenuClass($this->getMenu()->getCssClass()) + ->setMenuCode($this->getData('menu')) + ->setTarget($node->getTarget()); + + return $nodeBlock; + } + + /** + * @param array $nodes + * @param NodeRepositoryInterface $parentNode + * @param int $level + * @return Menu + */ + private function getSubmenuBlock($nodes, $parentNode, $level = 0) + { + $block = clone $this; + + $block->setSubmenuNodes($nodes) + ->setParentNode($parentNode) + ->setLevel($level); + + $block->setTemplateContext($block); + $block->setTemplate($this->submenuTemplate); + + return $block; + } + + /** + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function fetchData() + { + $nodes = $this->nodeRepository->getByMenu($this->loadMenu()->getId()); + $result = []; + $types = []; + foreach ($nodes as $node) { + $level = $node->getLevel(); + $parent = $node->getParentId() ?: 0; + if (!isset($result[$level])) { + $result[$level] = []; + } + if (!isset($result[$level][$parent])) { + $result[$level][$parent] = []; + } + $result[$level][$parent][] = $node; + $type = $node->getType(); + if (!isset($types[$type])) { + $types[$type] = []; + } + $types[$type][] = $node; + } + $this->nodes = $result; + + foreach ($types as $type => $nodes) { + $this->nodeTypeProvider->prepareData($type, $nodes); + } + } + + /** + * @param $node + * @param $level + * @return mixed + */ + private function renderNode($node, $level) + { + $type = $node->getType(); + return $this->nodeTypeProvider->render($type, $node->getId(), $level); + } + + /** + * @param string $template + * @return string + */ + private function getMenuTemplate($template) + { + return $this->templateResolver->getMenuTemplate( + $this, + $this->getData('menu'), + $template + ); + } +} diff --git a/Block/Menu/Edit.php b/Block/Menu/Edit.php new file mode 100755 index 0000000..4aa4605 --- /dev/null +++ b/Block/Menu/Edit.php @@ -0,0 +1,56 @@ +_blockGroup = 'Ecomteck_Megamenu'; + $this->_controller = 'menu'; + $this->_mode = 'edit'; + parent::_construct(); + + $this->buttonList->add( + 'save_and_continue', + [ + 'label' => __('Save and Continue Edit'), + 'class' => 'save', + 'data_attribute' => [ + 'mage-init' => [ + 'button' => [ + 'event' => 'saveAndContinueEdit', + 'target' => '#edit_form' + ] + ] + ] + ], + -100 + ); + } +} diff --git a/Block/Menu/Edit/Form.php b/Block/Menu/Edit/Form.php new file mode 100755 index 0000000..e771883 --- /dev/null +++ b/Block/Menu/Edit/Form.php @@ -0,0 +1,66 @@ +_formFactory->create( + ['data' => ['id' => 'edit_form', 'action' => $this->getData('action'), 'method' => 'post']] + ); + $form->setUseContainer(true); + + $menu = $this->_coreRegistry->registry(Edit::REGISTRY_CODE); + if ($menu) { + $form->addField('menu_id', 'hidden', ['name' => 'id']); + } + + $this->setForm($form); + return $this; + } + + /** + * @return $this|Generic + */ + protected function _initFormValues() + { + $menu = $this->_coreRegistry->registry(Edit::REGISTRY_CODE); + + if ($menu) { + $this->getForm()->setValues($menu->getData()); + } + return $this; + } +} diff --git a/Block/Menu/Edit/Tab/Main.php b/Block/Menu/Edit/Tab/Main.php new file mode 100755 index 0000000..720ef4b --- /dev/null +++ b/Block/Menu/Edit/Tab/Main.php @@ -0,0 +1,157 @@ +_formFactory->create(); + + $form->setHtmlIdPrefix('menu_'); + + $fieldSet = $form->addFieldset( + 'menu_fieldset', + ['legend' => __('Menu Data'), 'class' => 'fieldset-wide'] + ); + + $fieldSet->addField( + 'title', + 'text', + [ + 'name' => 'title', + 'label' => __('Title'), + 'class' => 'required', + ] + ); + + $fieldSet->addField( + 'identifier', + 'text', + [ + 'name' => 'identifier', + 'label' => __('Identifier'), + 'class' => 'required', + ] + ); + + $fieldSet->addField( + 'css_class', + 'text', + [ + 'name' => 'css_class', + 'label' => __('Menu Main CSS Class'), + 'class' => 'required', + ] + ); + + $values = []; + foreach ($this->_storeManager->getStores(false) as $storeId => $store) { + $values[] = [ + 'label' => $store->getName(), + 'value' => $storeId, + ]; + } + + $fieldSet->addField( + 'stores', + 'multiselect', + [ + 'name' => 'stores', + 'label' => __('Store View'), + 'class' => 'required', + 'values' => $values, + ] + ); + + $this->setForm($form); + } + + /** + * @return Generic|void + */ + protected function _initFormValues() + { + $menu = $this->_coreRegistry->registry(Edit::REGISTRY_CODE); + if ($menu) { + $menu->setData('stores', $menu->getStores()); + $this->getForm()->setValues($menu->getData()); + } + } + + /** + * Return Tab label + * + * @return string + * @api + */ + public function getTabLabel() + { + return __('Main information'); + } + + /** + * Return Tab title + * + * @return string + * @api + */ + public function getTabTitle() + { + return __('Main information'); + } + + /** + * Can show tab in tabs + * + * @return boolean + * @api + */ + public function canShowTab() + { + return true; + } + + /** + * Tab is hidden + * + * @return boolean + * @api + */ + public function isHidden() + { + return false; + } +} diff --git a/Block/Menu/Edit/Tab/Nodes.php b/Block/Menu/Edit/Tab/Nodes.php new file mode 100755 index 0000000..d1ce71d --- /dev/null +++ b/Block/Menu/Edit/Tab/Nodes.php @@ -0,0 +1,194 @@ +registry = $registry; + $this->nodeRepository = $nodeRepository; + $this->nodeTypeProvider = $nodeTypeProvider; + } + + /** + * @return array|void + */ + public function renderNodes() + { + $menu = $this->registry->registry(Edit::REGISTRY_CODE); + $data = []; + if ($menu) { + $nodes = $this->nodeRepository->getByMenu($menu->getId()); + if (!empty($nodes)) { + foreach ($nodes as $node) { + $level = $node->getLevel(); + $parent = $node->getParentId() ?: 0; + if (!isset($data[$level])) { + $data[$level] = []; + } + if (!isset($data[$level][$parent])) { + $data[$level][$parent] = []; + } + $data[$level][$parent][] = $node; + } + return $this->renderNodeList(0, null, $data); + } + } + return $data; + } + + /** + * Return Tab label + * + * @return string + * @api + */ + public function getTabLabel() + { + return __("Nodes"); + } + + /** + * Return Tab title + * + * @return string + * @api + */ + public function getTabTitle() + { + return __("Nodes"); + } + + /** + * Can show tab in tabs + * + * @return boolean + * @api + */ + public function canShowTab() + { + return true; + } + + /** + * Tab is hidden + * + * @return boolean + * @api + */ + public function isHidden() + { + return false; + } + + /** + * @param $level + * @param $parent + * @param $data + * @return array|void + */ + private function renderNodeList($level, $parent, $data) + { + if (is_null($parent)) { + $parent = 0; + } + if (empty($data[$level])) { + return; + } + if (empty($data[$level][$parent])) { + return; + } + $nodes = $data[$level][$parent]; + + $menu = []; + foreach ($nodes as $node) { + $menu[] = [ + 'type' => $node->getType(), + 'content' => $node->getContent(), + 'classes' => $node->getClasses(), + 'target' => $node->getTarget(), + 'id' => $node->getId(), + 'title' => $node->getTitle(), + 'columns' => $this->renderNodeList($level + 1, $node->getId(), $data) ? $this->renderNodeList($level + 1, $node->getId(), $data) : [] + ]; + } + return $menu; + } + + /** + * @return array + */ + public function getNodeForms() + { + return $this->nodeTypeProvider->getEditForms(); + } + + /** + * @return array + */ + public function getNodeLabels() + { + return $this->nodeTypeProvider->getLabels(); + } +} diff --git a/Block/Menu/Edit/Tabs.php b/Block/Menu/Edit/Tabs.php new file mode 100755 index 0000000..a5bca18 --- /dev/null +++ b/Block/Menu/Edit/Tabs.php @@ -0,0 +1,40 @@ +setId('menu_tabs'); + $this->setDestElementId('edit_form'); + $this->setTitle(__('Menu')); + } +} diff --git a/Block/NodeType/AbstractNode.php b/Block/NodeType/AbstractNode.php new file mode 100755 index 0000000..0b7e0b3 --- /dev/null +++ b/Block/NodeType/AbstractNode.php @@ -0,0 +1,168 @@ +addNodeAttribute(self::NAME_CODE, 'Node name', 'wysiwyg'); + $this->addNodeAttribute(self::CLASSES_CODE, 'Node CSS classes', 'text'); + $this->templateResolver = $templateResolver; + } + + /** + * @return string + */ + public function getNodeType() + { + if (!$this->nodeType) { + return strtolower(__CLASS__); + } + + return strtolower($this->nodeType); + } + + /** + * @return array + */ + public function getNodeAttributes() + { + return $this->nodeAttributes; + } + + /** + * @param $key + * + * @return DataObject + */ + public function getNodeAttribute($key) + { + if (array_key_exists($key, $this->nodeAttributes)) { + return $this->nodeAttributes[$key]; + } + + return new DataObject(); + } + + /** + * @param $key + * @param $label + * @param $type + */ + public function addNodeAttribute($key, $label, $type) + { + $data = [ + 'id' => $key . '_' . $this->nodeType, + 'label' => $label, + 'type' => $type, + 'code' => $key + ]; + $this->nodeAttributes[$key] = new DataObject($data); + } + + /** + * @param $key + * + * @return bool + */ + public function removeNodeAttribute($key) + { + if (array_key_exists($key, $this->nodeAttributes)) { + unset($this->nodeAttributes[$key]); + + return true; + } + + return false; + } + + /** + * @return bool + */ + public function isViewAllLinkAllowed() + { + return $this->viewAllLink; + } + + /** + * @return string + */ + protected function _toHtml() + { + $template = $this->templateResolver->getMenuTemplate( + $this, + $this->getMenuCode(), + $this->defaultTemplate + ); + $this->setTemplate($template); + + return parent::_toHtml(); + } +} diff --git a/Block/NodeType/Category.php b/Block/NodeType/Category.php new file mode 100755 index 0000000..793b04b --- /dev/null +++ b/Block/NodeType/Category.php @@ -0,0 +1,234 @@ +coreRegistry = $coreRegistry; + $this->_categoryModel = $categoryModel; + } + + /** + * @return \Magento\Catalog\Model\Category|null + */ + public function getCurrentCategory() + { + return $this->coreRegistry->registry('current_category'); + } + + /** + * @return array + */ + public function getNodeCacheKeyInfo() + { + $info = [ + 'module_' . $this->getRequest()->getModuleName(), + 'controller_' . $this->getRequest()->getControllerName(), + 'route_' . $this->getRequest()->getRouteName(), + 'action_' . $this->getRequest()->getActionName() + ]; + + $category = $this->getCurrentCategory(); + if ($category) { + $info[] = 'category_' . $category->getId(); + } + + return $info; + } + + /** + * @return array|mixed|string + * @throws \Exception + */ + public function getJsonConfig() + { + $data = $this->_categoryModel->fetchConfigData(); + + return $data; + } + + /** + * @param array $nodes + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function fetchData(array $nodes) + { + $storeId = $this->_storeManager->getStore()->getId(); + + list($this->nodes, $this->categoryUrls, $this->categories) = $this->_categoryModel->fetchData($nodes, $storeId); + } + + /** + * @param int $nodeId + * @return bool + * @throws \InvalidArgumentException + */ + public function isCurrentCategory($nodeId) + { + if (!isset($this->nodes[$nodeId])) { + throw new \InvalidArgumentException('Invalid node identifier specified'); + } + + $node = $this->nodes[$nodeId]; + $categoryId = (int) $node->getContent(); + $currentCategory = $this->getCurrentCategory(); + + return $currentCategory + ? $currentCategory->getId() == $categoryId + : false; + } + + /** + * @param $nodeId + * @param null $storeId + * @return bool|string + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function getCategoryUrl($nodeId, $storeId = null) + { + if (!isset($this->nodes[$nodeId])) { + throw new \InvalidArgumentException('Invalid node identifier specified'); + } + + $node = $this->nodes[$nodeId]; + $categoryId = (int) $node->getContent(); + + if (isset($this->categoryUrls[$categoryId])) { + $baseUrl = $this->_storeManager->getStore($storeId)->getBaseUrl(); + $categoryUrlPath = $this->categoryUrls[$categoryId]; + + return $baseUrl . $categoryUrlPath; + } + + return false; + } + + /** + * @param int $nodeId + * + * @return object|false + * @throws \InvalidArgumentException + */ + public function getCategory(int $nodeId) + { + if (!isset($this->nodes[$nodeId])) { + throw new \InvalidArgumentException('Invalid node identifier specified'); + } + + $node = $this->nodes[$nodeId]; + $categoryId = (int) $node->getContent(); + + if (isset($this->categories[$categoryId])) { + return $this->categories[$categoryId]; + } + + return false; + } + + /** + * @param int $nodeId + * @param int $level + * @param null $storeId + * @return string + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function getHtml($nodeId, $level, $storeId = null) + { + $classes = $level == 0 ? 'level-top' : ''; + $node = $this->nodes[$nodeId]; + $url = $this->getCategoryUrl($nodeId, $storeId); + $title = $node->getTitle(); + + return <<$title +HTML; + } + + /** + * @return \Magento\Framework\Phrase + */ + public function getLabel() + { + return __("Category"); + } +} diff --git a/Block/NodeType/CmsBlock.php b/Block/NodeType/CmsBlock.php new file mode 100755 index 0000000..abb687d --- /dev/null +++ b/Block/NodeType/CmsBlock.php @@ -0,0 +1,140 @@ +filterProvider = $filterProvider; + $this->_cmsBlockModel = $cmsBlockModel; + } + + /** + * @return string + */ + public function getJsonConfig() + { + $data = $this->_cmsBlockModel->fetchConfigData(); + + return $data; + } + + /** + * @param array $nodes + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function fetchData(array $nodes) + { + $storeId = $this->_storeManager->getStore()->getId(); + + list($this->nodes, $this->content) = $this->_cmsBlockModel->fetchData($nodes, $storeId); + } + + /** + * @param int $nodeId + * @param int $level + * @return mixed|string + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function getHtml($nodeId, $level) + { + $node = $this->nodes[$nodeId]; + $storeId = $this->_storeManager->getStore()->getId(); + + if (isset($this->content[$node->getContent()])) { + $content = $this->content[$node->getContent()]; + $content = $this->filterProvider->getBlockFilter()->setStoreId($storeId)->filter($content); + + return $content; + } + + return ''; + } + + /** + * @return \Magento\Framework\Phrase + */ + public function getLabel() + { + return __("Cms Block"); + } +} diff --git a/Block/NodeType/CmsPage.php b/Block/NodeType/CmsPage.php new file mode 100755 index 0000000..e4986af --- /dev/null +++ b/Block/NodeType/CmsPage.php @@ -0,0 +1,205 @@ +_cmsPageModel = $cmsPageModel; + $this->page = $page; + } + + /** + * @return array + */ + public function getNodeCacheKeyInfo() + { + $info = []; + $pageId = $this->getRequest()->getParam('page_id'); + + if ($pageId) { + $info[] = 'cms_page_' . $pageId; + } + + return $info; + } + + /** + * @return string + */ + public function getJsonConfig() + { + $data = $this->_cmsPageModel->fetchConfigData(); + + return $data; + } + + /** + * @param array $nodes + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function fetchData(array $nodes) + { + $storeId = $this->_storeManager->getStore()->getId(); + + list($this->nodes, $this->pageIds, $this->pageUrls) = $this->_cmsPageModel->fetchData($nodes, $storeId); + } + + /** + * @param int $nodeId + * @return bool + * @throws \InvalidArgumentException + */ + public function isCurrentPage($nodeId) + { + if (!isset($this->nodes[$nodeId])) { + throw new \InvalidArgumentException('Invalid node identifier specified'); + } + + $node = $this->nodes[$nodeId]; + $nodeContent = $node->getContent(); + + return isset($this->pageIds[$nodeContent]) + ? $this->page->getId() == $this->pageIds[$nodeContent] + : false; + } + + /** + * @param $nodeId + * @param null $storeId + * @return bool|string + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function getPageUrl($nodeId, $storeId = null) + { + if (!isset($this->nodes[$nodeId])) { + throw new \InvalidArgumentException('Invalid node identifier specified'); + } + + $node = $this->nodes[$nodeId]; + $nodeContent = $node->getContent(); + + if (isset($this->pageIds[$nodeContent])) { + $pageId = $this->pageIds[$nodeContent]; + $baseUrl = $this->_storeManager->getStore($storeId)->getBaseUrl(); + $pageUrlPath = (isset($this->pageUrls[$pageId])) + ? $this->pageUrls[$pageId] + :''; + return $baseUrl . $pageUrlPath; + } + + return false; + } + + /** + * @param int $nodeId + * @param int $level + * @return string + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function getHtml($nodeId, $level) + { + $classes = $level == 0 ? 'level-top' : ''; + $node = $this->nodes[$nodeId]; + + if (isset($this->pageIds[$node->getContent()])) { + $pageId = $this->pageIds[$node->getContent()]; + $url = $this->_storeManager->getStore()->getBaseUrl() . $this->pageUrls[$pageId]; + } else { + $url = $this->_storeManager->getStore()->getBaseUrl(); + } + + $title = $node->getTitle(); + + return <<$title +HTML; + } + + /** + * @return \Magento\Framework\Phrase + */ + public function getLabel() + { + return __("Cms Page link"); + } +} diff --git a/Block/NodeType/CustomUrl.php b/Block/NodeType/CustomUrl.php new file mode 100755 index 0000000..f667a64 --- /dev/null +++ b/Block/NodeType/CustomUrl.php @@ -0,0 +1,140 @@ +addNodeAttribute(self::NAME_TARGET, 'Node target blank', 'checkbox'); + $this->_customUrlModel = $customUrlModel; + } + + /** + * @inheritDoc + */ + public function getJsonConfig() + { + $data = [ + "ecomteckMenuSimpleField" => [ + "type" => "custom_url" + ] + ]; + return $data; + } + + /** + * @param array $nodes + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function fetchData(array $nodes) + { + $storeId = $this->_storeManager->getStore()->getId(); + + $this->nodes = $this->_customUrlModel->fetchData($nodes, $storeId); + } + + /** + * @param int $nodeId + * @param int $level + * @return string + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function getHtml($nodeId, $level) + { + $classes = $level == 0 ? 'level-top' : ''; + $node = $this->nodes[$nodeId]; + $nodeContent = $node->getContent(); + $title = $node->getTitle(); + + if (!$this->isExternalUrl($nodeContent)) { + $url = $this->_storeManager->getStore()->getBaseUrl() . $nodeContent; + } else { + $url = $nodeContent; + } + + return <<$title +HTML; + } + + /** + * @param string|null $url + * + * @return bool + */ + private function isExternalUrl($url) + { + return filter_var($url, FILTER_VALIDATE_URL); + } + + /** + * @return \Magento\Framework\Phrase + */ + public function getLabel() + { + return __("Custom Url"); + } +} diff --git a/Block/NodeType/Product.php b/Block/NodeType/Product.php new file mode 100755 index 0000000..c8c3a5a --- /dev/null +++ b/Block/NodeType/Product.php @@ -0,0 +1,287 @@ +coreRegistry = $coreRegistry; + $this->productModel = $productModel; + $this->priceHelper = $priceHelper; + } + + /** + * @return \Magento\Catalog\Model\Product|null + */ + public function getCurrentProduct() + { + return $this->coreRegistry->registry('current_product'); + } + + /** + * @return string + */ + public function getJsonConfig() + { + $data = $this->productModel->fetchConfigData(); + + return $data; + } + + /** + * @param array $nodes + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function fetchData(array $nodes) + { + $storeId = $this->_storeManager->getStore()->getId(); + + list( + $this->nodes, + $this->productUrls, + $this->productPrices, + $this->productImages, + $this->productTitles + ) = $this->productModel->fetchData($nodes, $storeId); + } + + /** + * @param int $nodeId + * @return bool + * @throws \InvalidArgumentException + */ + public function isCurrentProduct($nodeId) + { + if (!isset($this->nodes[$nodeId])) { + throw new \InvalidArgumentException('Invalid node identifier specified'); + } + + $node = $this->nodes[$nodeId]; + $productId = (int)$node->getContent(); + $currentProduct = $this->getCurrentProduct(); + + return $currentProduct + ? $currentProduct->getId() == $productId + : false; + } + + /** + * @param $nodeId + * @param null $storeId + * @return bool|string + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function getProductUrl($nodeId, $storeId = null) + { + $productUrlPath = $this->getProductData($this->productUrls, $nodeId); + + if ($productUrlPath) { + $baseUrl = $this->_storeManager->getStore($storeId)->getBaseUrl(); + + return $baseUrl . $productUrlPath; + } + + return false; + } + + /** + * @param int $nodeId + * @return double|false + * @throws \InvalidArgumentException + */ + public function getProductPrice($nodeId) + { + return $this->getProductData($this->productPrices, $nodeId); + } + + /** + * @param int $nodeId + * @return null|string + */ + public function getProductImage($nodeId) + { + $image = $this->getProductData($this->productImages, $nodeId); + + if (!$image) { + return null; + } + + return $this->getMediaUrl('catalog/product' . $image); + } + + /** + * @param array $data + * @param int $nodeId + * @return false|string|double + */ + public function getProductData($data, $nodeId) + { + if (!isset($this->nodes[$nodeId])) { + throw new \InvalidArgumentException('Invalid node identifier specified'); + } + + $node = $this->nodes[$nodeId]; + $productId = (int)$node->getContent(); + + if (isset($data[$productId])) { + return $data[$productId]; + } + + return false; + } + + /** + * @param int $nodeId + * @param int $level + * @param null $storeId + * @return string + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function getHtml($nodeId, $level, $storeId = null) + { + $classes = $level == 0 ? 'level-top' : ''; + $node = $this->nodes[$nodeId]; + $url = $this->getProductUrl($nodeId, $storeId); + $title = $node->getTitle(); + + return <<$title +HTML; + } + + /** + * @param $path + * @return string + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function getMediaUrl($path) + { + if (!$this->mediaUrl) { + $this->mediaUrl = $this->_storeManager->getStore() + ->getBaseUrl(UrlInterface::URL_TYPE_MEDIA); + } + + return $this->mediaUrl . $path; + } + + /** + * @return \Magento\Framework\Phrase + */ + public function getLabel() + { + return __("Product"); + } + + /** + * @param int $nodeId + * @return false|string + */ + public function getProductTitle($nodeId) + { + return $this->getProductData($this->productTitles, $nodeId); + } + + /** + * @param int $nodeId + * @return float|string + */ + public function getFormattedProductPrice($nodeId) + { + $productPrice = $this->getProductPrice($nodeId); + + return $this->priceHelper->currency($productPrice, true, false); + } +} diff --git a/Block/NodeType/Wrapper.php b/Block/NodeType/Wrapper.php new file mode 100755 index 0000000..ad78dad --- /dev/null +++ b/Block/NodeType/Wrapper.php @@ -0,0 +1,121 @@ +wrapperModel = $wrapperModel; + + parent::__construct($context, $templateResolver, $data); + } + + /** + * @inheritDoc + */ + public function getJsonConfig() + { + return [ + "ecomteckMenuSimpleField" => [ + "type" => "wrapper" + ] + ]; + } + + /** + * @param array $nodes + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function fetchData(array $nodes) + { + $this->nodes = $this->wrapperModel->fetchData( + $nodes, + $this->_storeManager->getStore()->getId() + ); + } + + /** + * @param int $nodeId + * @param int $level + * + * @return string + */ + public function getHtml($nodeId, $level) + { + $classes = $level == 0 ? 'level-top' : ''; + $node = $this->nodes[$nodeId]; + $nodeClass = $node->getClasses(); + + return << +HTML; + } + + /** + * @return Phrase + */ + public function getLabel() + { + return __("Wrapper"); + } +} diff --git a/Controller/Adminhtml/Menu/Create.php b/Controller/Adminhtml/Menu/Create.php new file mode 100755 index 0000000..b3120b6 --- /dev/null +++ b/Controller/Adminhtml/Menu/Create.php @@ -0,0 +1,55 @@ +resultFactory->create(ResultFactory::TYPE_PAGE); + $result->setActiveMenu('Ecomteck_Megamenu::menus'); + $result->getConfig()->getTitle()->prepend(__('Create new menu')); + return $result; + } + + /** + * Is allowed + * @return bool + */ + protected function _isAllowed() + { + return $this->_authorization->isAllowed('Ecomteck_Megamenu::menus'); + } +} diff --git a/Controller/Adminhtml/Menu/Delete.php b/Controller/Adminhtml/Menu/Delete.php new file mode 100755 index 0000000..a1f5077 --- /dev/null +++ b/Controller/Adminhtml/Menu/Delete.php @@ -0,0 +1,134 @@ +menuRepository = $menuRepository; + $this->nodeRepository = $nodeRepository; + $this->filterBuilderFactory = $filterBuilderFactory; + $this->filterGroupBuilderFactory = $filterGroupBuilderFactory; + $this->searchCriteriaBuilderFactory = $searchCriteriaBuilderFactory; + } + + /** + * Dispatch request + * + * @return \Magento\Framework\Controller\ResultInterface|ResponseInterface + * @throws \Magento\Framework\Exception\NotFoundException + */ + public function execute() + { + $id = $this->getRequest()->getParam('id'); + + try { + $menu = $this->menuRepository->getById($id); + $this->menuRepository->deleteById($id); + + $filterBuilder = $this->filterBuilderFactory->create(); + $filter = $filterBuilder->setField('menu_id')->setValue($id)->setConditionType('eq')->create(); + + $filterGroupBuilder = $this->filterGroupBuilderFactory->create(); + $filterGroup = $filterGroupBuilder->addFilter($filter)->create(); + + $searchCriteriaBuilder = $this->searchCriteriaBuilderFactory->create(); + $searchCriteria = $searchCriteriaBuilder->setFilterGroups([$filterGroup])->create(); + + $nodes = $this->nodeRepository->getList($searchCriteria); + foreach ($nodes->getItems() as $node) { + $this->nodeRepository->delete($node); + } + $this->messageManager->addSuccessMessage(__("Menu %1 and it's nodes removed", $menu->getTitle())); + } catch (CouldNotDeleteException $e) { + $this->messageManager->addErrorMessage($e->getMessage()); + } + + $redirect = $this->resultRedirectFactory->create(); + $redirect->setPath('*/*/index'); + return $redirect; + } + + /** + * Is allowed + * @return bool + */ + protected function _isAllowed() + { + return $this->_authorization->isAllowed('Ecomteck_Megamenu::menus'); + } +} diff --git a/Controller/Adminhtml/Menu/Edit.php b/Controller/Adminhtml/Menu/Edit.php new file mode 100755 index 0000000..67f1b51 --- /dev/null +++ b/Controller/Adminhtml/Menu/Edit.php @@ -0,0 +1,99 @@ +menuRepository = $menuRepository; + $this->registry = $registry; + } + + /** + * Dispatch request + * + * @return \Magento\Framework\Controller\ResultInterface|ResponseInterface + * @throws \Magento\Framework\Exception\NotFoundException + */ + public function execute() + { + $id = $this->getRequest()->getParam('id'); + try { + $model = $this->menuRepository->getById($id); + $this->registry->register(self::REGISTRY_CODE, $model); + $result = $this->resultFactory->create(ResultFactory::TYPE_PAGE); + $result->setActiveMenu('Ecomteck_Megamenu::menus'); + $result->getConfig()->getTitle()->prepend(__('Edit Menu %1', $model->getTitle())); + return $result; + } catch (NoSuchEntityException $e) { + $result = $this->resultRedirectFactory->create(); + $result->setPath('*/*/index'); + return $result; + } + } + + /** + * Is allowed + * @return bool + */ + protected function _isAllowed() + { + return $this->_authorization->isAllowed('Ecomteck_Megamenu::menus'); + } +} diff --git a/Controller/Adminhtml/Menu/Index.php b/Controller/Adminhtml/Menu/Index.php new file mode 100755 index 0000000..4df3680 --- /dev/null +++ b/Controller/Adminhtml/Menu/Index.php @@ -0,0 +1,67 @@ +resultPageFactory = $resultPageFactory; + return parent::__construct($context); + } + + /** + * @return \Magento\Framework\App\ResponseInterface|\Magento\Framework\Controller\ResultInterface|\Magento\Framework\View\Result\Page + */ + public function execute() + { + return $this->resultPageFactory->create(); + } + + /** + * Is allowed + * @return bool + */ + protected function _isAllowed() + { + return $this->_authorization->isAllowed('Ecomteck_Megamenu::menus'); + } +} diff --git a/Controller/Adminhtml/Menu/Save.php b/Controller/Adminhtml/Menu/Save.php new file mode 100755 index 0000000..55eb74b --- /dev/null +++ b/Controller/Adminhtml/Menu/Save.php @@ -0,0 +1,292 @@ +menuRepository = $menuRepository; + $this->nodeRepository = $nodeRepository; + $this->filterBuilderFactory = $filterBuilderFactory; + $this->filterGroupBuilderFactory = $filterGroupBuilderFactory; + $this->searchCriteriaBuilderFactory = $searchCriteriaBuilderFactory; + $this->nodeFactory = $nodeFactory; + $this->menuFactory = $menuFactory; + $this->productRepository = $productRepository; + } + + + /** + * Dispatch request + * + * @return \Magento\Framework\Controller\ResultInterface|ResponseInterface + * @throws \Magento\Framework\Exception\NotFoundException + */ + public function execute() + { + $id = $this->getRequest()->getParam('id'); + if ($id) { + $menu = $this->menuRepository->getById($id); + } else { + $menu = $this->menuFactory->create(); + } + $menu->setTitle($this->getRequest()->getParam('title')); + $menu->setIdentifier($this->getRequest()->getParam('identifier')); + $menu->setCssClass($this->getRequest()->getParam('css_class')); + $menu->setIsActive(1); + $menu = $this->menuRepository->save($menu); + + if (!$id) { + $id = $menu->getId(); + } + + $menu->saveStores($this->getRequest()->getParam('stores')); + + $nodes = $this->getRequest()->getParam('serialized_nodes'); + if (!empty($nodes)) { + $nodes = json_decode($nodes, true); + $nodes = $this->_convertTree($nodes, '#'); + + if (!empty($nodes)) { + + $filterBuilder = $this->filterBuilderFactory->create(); + $filter = $filterBuilder->setField('menu_id')->setValue($id)->setConditionType('eq')->create(); + + $filterGroupBuilder = $this->filterGroupBuilderFactory->create(); + $filterGroup = $filterGroupBuilder->addFilter($filter)->create(); + + $searchCriteriaBuilder = $this->searchCriteriaBuilderFactory->create(); + $searchCriteria = $searchCriteriaBuilder->setFilterGroups([$filterGroup])->create(); + + $oldNodes = $this->nodeRepository->getList($searchCriteria)->getItems(); + + $existingNodes = []; + foreach ($oldNodes as $node) { + $existingNodes[$node->getId()] = $node; + } + + $nodesToDelete = []; + foreach ($existingNodes as $nodeId => $noe) { + $nodesToDelete[$nodeId] = true; + } + + $nodeMap = []; + + foreach ($nodes as $node) { + $nodeId = $node['id']; + $matches = []; + if (preg_match('/^node_([0-9]+)$/', $nodeId, $matches)) { + $nodeId = $matches[1]; + unset($nodesToDelete[$nodeId]); + $nodeMap[$node['id']] = $existingNodes[$nodeId]; + } else { + $nodeObject = $this->nodeFactory->create(); + $nodeObject->setMenuId($id); + $nodeObject = $this->nodeRepository->save($nodeObject); + $nodeMap[$nodeId] = $nodeObject; + } + } + + foreach (array_keys($nodesToDelete) as $nodeId) { + $this->nodeRepository->deleteById($nodeId); + } + + $path = [ + '#' => 0, + ]; + foreach ($nodes as $node) { + if ($node['type'] == 'product' && !$this->validateProductNode($node)) { + continue; + } + $nodeObject = $nodeMap[$node['id']]; + + $parents = array_keys($path); + $parent = array_pop($parents); + while ($parent != $node['parent']) { + array_pop($path); + $parent = array_pop($parents); + } + + $level = count($path) - 1; + $position = $path[$node['parent']]++; + + if ($node['parent'] == '#') { + $nodeObject->setParentId(null); + } else { + $nodeObject->setParentId($nodeMap[$node['parent']]->getId()); + } + + $nodeObject->setType($node['type']); + if (isset($node['classes'])) { + $nodeObject->setClasses($node['classes']); + } + if (isset($node['content'])) { + $nodeObject->setContent($node['content']); + } + if (isset($node['target'])) { + $nodeObject->setTarget($node['target']); + } + $nodeObject->setMenuId($id); + $nodeObject->setTitle($node['title']); + $nodeObject->setIsActive(1); + $nodeObject->setLevel($level); + $nodeObject->setPosition($position); + + $this->nodeRepository->save($nodeObject); + + $path[$node['id']] = 0; + } + } + } + + $redirect = $this->resultRedirectFactory->create(); + $redirect->setPath('*/*/index'); + + if ($this->getRequest()->getParam('back')) { + $redirect->setPath('*/*/edit', ['id' => $menu->getId(), '_current' => true]); + } + + return $redirect; + } + + /** + * Is allowed + * @return bool + */ + protected function _isAllowed() + { + return $this->_authorization->isAllowed('Ecomteck_Megamenu::menus'); + } + + /** + * @param $nodes + * @param $parent + * @return array + */ + protected function _convertTree($nodes, $parent) + { + $convertedTree = []; + if (!empty($nodes)) { + foreach ($nodes as $node) { + $node['parent'] = $parent; + $convertedTree[] = $node; + $convertedTree = array_merge($convertedTree, $this->_convertTree($node['columns'], $node['id'])); + } + } + return $convertedTree; + } + + /** + * @param array $node + * @return bool + */ + private function validateProductNode(array $node) + { + try { + $this->productRepository->getById($node['content']); + } catch (NoSuchEntityException $e) { + $this->messageManager->addErrorMessage(__('Product does not exist')); + return false; + } + + return true; + } +} diff --git a/Model/Menu.php b/Model/Menu.php new file mode 100755 index 0000000..b16230b --- /dev/null +++ b/Model/Menu.php @@ -0,0 +1,208 @@ +_init(\Ecomteck\Megamenu\Model\ResourceModel\Menu::class); + } + + /** + * Get Identities + * + * @return array|string[] + */ + public function getIdentities() + { + return [self::CACHE_TAG . '_' . $this->getId()]; + } + + /** + * Get stores + * + * @return array + */ + public function getStores() + { + $connection = $this->getResource()->getConnection(); + $select = $connection->select() + ->from($this->getResource()->getTable('ecomteck_menu_store'), ['store_id']) + ->where('menu_id = ?', $this->getId()); + + return $connection->fetchCol($select); + } + + /** + * Save stores + * + * @param array $stores + */ + public function saveStores(array $stores) + { + $connection = $this->getResource()->getConnection(); + $connection->beginTransaction(); + $table = $this->getResource()->getTable('ecomteck_menu_store'); + $connection->delete($table, ['menu_id = ?' => $this->getId()]); + foreach ($stores as $store) { + $connection->insert($table, ['menu_id' => $this->getId(), 'store_id' => $store]); + } + $connection->commit(); + } + + /** + * @inheritdoc + */ + public function getMenuId() + { + return $this->_getData(MenuInterface::MENU_ID); + } + + /** + * @inheritdoc + */ + public function setMenuId($menuId) + { + return $this->setData(MenuInterface::MENU_ID, $menuId); + } + + /** + * @inheritdoc + */ + public function getTitle() + { + return $this->_getData(MenuInterface::TITLE); + } + + /** + * @inheritdoc + */ + public function setTitle($title) + { + return $this->setData(MenuInterface::TITLE, $title); + } + + /** + * @inheritdoc + */ + public function getIdentifier() + { + return $this->_getData(MenuInterface::IDENTIFIER); + } + + /** + * @inheritdoc + */ + public function setIdentifier($identifier) + { + return $this->setData(MenuInterface::IDENTIFIER, $identifier); + } + + /** + * @inheritdoc + */ + public function getCreationTime() + { + return $this->_getData(MenuInterface::CREATION_TIME); + } + + /** + * @inheritdoc + */ + public function setCssClass($cssClass) + { + return $this->setData(MenuInterface::CSS_CLASS, $cssClass); + } + + /** + * @inheritdoc + */ + public function getCssClass() + { + return $this->_getData(MenuInterface::CSS_CLASS); + } + + /** + * @inheritdoc + */ + public function setCreationTime($creationTime) + { + return $this->setData(MenuInterface::CREATION_TIME, $creationTime); + } + + /** + * @inheritdoc + */ + public function getUpdateTime() + { + return $this->_getData(MenuInterface::UPDATE_TIME); + } + + /** + * @inheritdoc + */ + public function setUpdateTime($updateTime) + { + return $this->setData(MenuInterface::UPDATE_TIME, $updateTime); + } + + /** + * @inheritdoc + */ + public function getIsActive() + { + return $this->_getData(MenuInterface::IS_ACTIVE); + } + + /** + * @inheritdoc + */ + public function setIsActive($isActive) + { + return $this->setData(MenuInterface::IS_ACTIVE, $isActive); + } +} diff --git a/Model/Menu/Node.php b/Model/Menu/Node.php new file mode 100755 index 0000000..cf49197 --- /dev/null +++ b/Model/Menu/Node.php @@ -0,0 +1,261 @@ +_init(\Ecomteck\Megamenu\Model\ResourceModel\Menu\Node::class); + } + + /** + * @inheritdoc + */ + public function getIdentities() + { + return [self::CACHE_TAG . '_' . $this->getId()]; + } + + /** + * @inheritdoc + */ + public function getNodeId() + { + return $this->_getData(NodeInterface::NODE_ID); + } + + /** + * @inheritdoc + */ + public function setNodeId($nodeId) + { + return $this->setData(NodeInterface::NODE_ID, $nodeId); + } + + /** + * @inheritdoc + */ + public function getMenuId() + { + return $this->_getData(NodeInterface::MENU_ID); + } + + /** + * @inheritdoc + */ + public function setMenuId($menuId) + { + return $this->setData(NodeInterface::MENU_ID, $menuId); + } + + /** + * @inheritdoc + */ + public function getType() + { + return $this->_getData(NodeInterface::TYPE); + } + + /** + * @inheritdoc + */ + public function setType($type) + { + return $this->setData(NodeInterface::TYPE, $type); + } + + /** + * @inheritdoc + */ + public function getContent() + { + return $this->_getData(NodeInterface::CONTENT); + } + + /** + * @inheritdoc + */ + public function setContent($content) + { + return $this->setData(NodeInterface::CONTENT, $content); + } + + /** + * @inheritdoc + */ + public function getClasses() + { + return $this->_getData(NodeInterface::CLASSES); + } + + /** + * @inheritdoc + */ + public function setClasses($classes) + { + return $this->setData(NodeInterface::CLASSES, $classes); + } + + /** + * @inheritdoc + */ + public function getParentId() + { + return $this->_getData(NodeInterface::PARENT_ID); + } + + /** + * @inheritdoc + */ + public function setParentId($parentId) + { + return $this->setData(NodeInterface::PARENT_ID, $parentId); + } + + /** + * @inheritdoc + */ + public function getPosition() + { + return $this->_getData(NodeInterface::POSITION); + } + + /** + * @inheritdoc + */ + public function setPosition($position) + { + return $this->setData(NodeInterface::POSITION, $position); + } + + /** + * @inheritdoc + */ + public function getLevel() + { + return $this->_getData(NodeInterface::LEVEL); + } + + /** + * @inheritdoc + */ + public function setLevel($level) + { + return $this->setData(NodeInterface::LEVEL, $level); + } + + /** + * @inheritdoc + */ + public function getTitle() + { + return $this->_getData(NodeInterface::TITLE); + } + + /** + * @inheritdoc + */ + public function setTitle($title) + { + return $this->setData(NodeInterface::TITLE, $title); + } + + /** + * @inheritdoc + */ + public function getTarget() + { + return $this->_getData(NodeInterface::TARGET); + } + + /** + * @inheritdoc + */ + public function setTarget($target) + { + return $this->setData(NodeInterface::TARGET, $target); + } + + /** + * @inheritdoc + */ + public function getCreationTime() + { + return $this->_getData(NodeInterface::CREATION_TIME); + } + + /** + * @inheritdoc + */ + public function setCreationTime($creationTime) + { + return $this->setData(NodeInterface::CREATION_TIME, $creationTime); + } + + /** + * @inheritdoc + */ + public function getUpdateTime() + { + return $this->_getData(NodeInterface::UPDATE_TIME); + } + + /** + * @inheritdoc + */ + public function setUpdateTime($updateTime) + { + return $this->setData(NodeInterface::UPDATE_TIME, $updateTime); + } + + /** + * @inheritdoc + */ + public function getIsActive() + { + return $this->_getData(NodeInterface::IS_ACTIVE); + } + + /** + * @inheritdoc + */ + public function setIsActive($isActive) + { + return $this->setData(NodeInterface::IS_ACTIVE, $isActive); + } +} diff --git a/Model/Menu/NodeRepository.php b/Model/Menu/NodeRepository.php new file mode 100755 index 0000000..ffac6df --- /dev/null +++ b/Model/Menu/NodeRepository.php @@ -0,0 +1,201 @@ +objectFactory = $objectFactory; + $this->collectionFactory = $collectionFactory; + $this->searchResultsFactory = $searchResultsFactory; + } + + /** + * @param NodeInterface $object + * @return NodeInterface + * @throws CouldNotSaveException + */ + public function save(NodeInterface $object) + { + try { + $object->save(); + } catch (Exception $e) { + throw new CouldNotSaveException($e->getMessage()); + } + return $object; + } + + /** + * @param int $id + * @return Node + * @throws NoSuchEntityException + */ + public function getById($id) + { + $object = $this->objectFactory->create(); + $object->load($id); + if (!$object->getId()) { + throw new NoSuchEntityException(__('Object with id "%1" does not exist.', $id)); + } + return $object; + } + + /** + * @param NodeInterface $object + * @return bool + * @throws CouldNotDeleteException + */ + public function delete(NodeInterface $object) + { + try { + $object->delete(); + } catch (Exception $exception) { + throw new CouldNotDeleteException(__($exception->getMessage())); + } + return true; + } + + /** + * @param int $id + * @return bool + * @throws CouldNotDeleteException + * @throws NoSuchEntityException + */ + public function deleteById($id) + { + return $this->delete($this->getById($id)); + } + + /** + * @param SearchCriteriaInterface $criteria + * @return \Magento\Framework\Api\SearchResultsInterface + */ + public function getList(SearchCriteriaInterface $criteria) + { + $searchResults = $this->searchResultsFactory->create(); + $searchResults->setSearchCriteria($criteria); + $collection = $this->collectionFactory->create(); + foreach ($criteria->getFilterGroups() as $filterGroup) { + $fields = []; + $conditions = []; + foreach ($filterGroup->getFilters() as $filter) { + $condition = $filter->getConditionType() ? $filter->getConditionType() : 'eq'; + $fields[] = $filter->getField(); + $conditions[] = [$condition => $filter->getValue()]; + } + if ($fields) { + $collection->addFieldToFilter($fields, $conditions); + } + } + $searchResults->setTotalCount($collection->getSize()); + $sortOrders = $criteria->getSortOrders(); + if ($sortOrders) { + /** @var SortOrder $sortOrder */ + foreach ($sortOrders as $sortOrder) { + $collection->addOrder( + $sortOrder->getField(), + ($sortOrder->getDirection() == SortOrder::SORT_ASC) ? 'ASC' : 'DESC' + ); + } + } + $collection->setCurPage($criteria->getCurrentPage()); + $collection->setPageSize($criteria->getPageSize()); + $objects = []; + foreach ($collection as $objectModel) { + $objects[] = $objectModel; + } + $searchResults->setItems($objects); + return $searchResults; + } + + /** + * @param int $menuId + * @return NodeInterface[] + */ + public function getByMenu($menuId) + { + $collection = $this->collectionFactory->create(); + $collection->addFilter('menu_id', $menuId); + $collection->addFilter('is_active', 1); + $collection->addOrder('level', AbstractCollection::SORT_ORDER_ASC); + $collection->addOrder('parent_id', AbstractCollection::SORT_ORDER_ASC); + $collection->addOrder('position', AbstractCollection::SORT_ORDER_ASC); + return $collection->getItems(); + } + + /** + * @param string $identifier + * @return NodeInterface[] + */ + public function getByIdentifier($identifier) + { + $collection = $this->collectionFactory->create(); + $collection->addFilter('main_table.is_active', 1); + $collection->addOrder('level', AbstractCollection::SORT_ORDER_ASC); + $collection->addOrder('parent_id', AbstractCollection::SORT_ORDER_ASC); + $collection->addOrder('position', AbstractCollection::SORT_ORDER_ASC); + $collection->join(['menu' => 'ecomteck_menu_menu'], 'main_table.menu_id = menu.menu_id', 'identifier'); + $collection->addFilter('identifier', $identifier); + return $collection->getItems(); + } +} diff --git a/Model/MenuRepository.php b/Model/MenuRepository.php new file mode 100755 index 0000000..baaf0a2 --- /dev/null +++ b/Model/MenuRepository.php @@ -0,0 +1,178 @@ +objectFactory = $objectFactory; + $this->collectionFactory = $collectionFactory; + $this->menuSearchResultsFactory = $menuSearchResults; + } + + /** + * @param MenuInterface $object + * @return MenuInterface + * @throws CouldNotSaveException + */ + public function save(MenuInterface $object) + { + try { + $object->save(); + } catch (\Exception $e) { + throw new CouldNotSaveException($e->getMessage()); + } + return $object; + } + + /** + * @param int $id + * @return Menu + * @throws NoSuchEntityException + */ + public function getById($id) + { + $object = $this->objectFactory->create(); + $object->load($id); + if (!$object->getId()) { + throw new NoSuchEntityException(__('Object with id "%1" does not exist.', $id)); + } + return $object; + } + + /** + * @param MenuInterface $object + * @return bool + * @throws CouldNotDeleteException + */ + public function delete(MenuInterface $object) + { + try { + $object->delete(); + } catch (Exception $exception) { + throw new CouldNotDeleteException(__($exception->getMessage())); + } + return true; + } + + /** + * @param int $id + * @return bool + * @throws CouldNotDeleteException + * @throws NoSuchEntityException + */ + public function deleteById($id) + { + return $this->delete($this->getById($id)); + } + + /** + * @inheritdoc + */ + public function getList(SearchCriteriaInterface $criteria) + { + $searchResults = $this->menuSearchResultsFactory->create(); + $searchResults->setSearchCriteria($criteria); + $collection = $this->collectionFactory->create(); + foreach ($criteria->getFilterGroups() as $filterGroup) { + $fields = []; + $conditions = []; + foreach ($filterGroup->getFilters() as $filter) { + $condition = $filter->getConditionType() ? $filter->getConditionType() : 'eq'; + $fields[] = $filter->getField(); + $conditions[] = [$condition => $filter->getValue()]; + } + if ($fields) { + $collection->addFieldToFilter($fields, $conditions); + } + } + $searchResults->setTotalCount($collection->getSize()); + $sortOrders = $criteria->getSortOrders(); + if ($sortOrders) { + /** @var SortOrder $sortOrder */ + foreach ($sortOrders as $sortOrder) { + $collection->addOrder( + $sortOrder->getField(), + ($sortOrder->getDirection() == SortOrder::SORT_ASC) ? 'ASC' : 'DESC' + ); + } + } + $collection->setCurPage($criteria->getCurrentPage()); + $collection->setPageSize($criteria->getPageSize()); + $objects = []; + foreach ($collection as $objectModel) { + $objects[] = $objectModel; + } + $searchResults->setItems($objects); + + return $searchResults; + } + + /** + * @param string $identifier + * @param int $storeId + * @return Menu + */ + public function get($identifier, $storeId) + { + $collection = $this->collectionFactory->create(); + $collection->addFilter('identifier', $identifier); + $collection->addFilter('is_active', 1); + $collection->join(['stores' => 'ecomteck_menu_store'], 'main_table.menu_id = stores.menu_id', 'store_id'); + $collection->addFilter('store_id', $storeId); + return $collection->getFirstItem(); + } +} diff --git a/Model/NodeType/AbstractNode.php b/Model/NodeType/AbstractNode.php new file mode 100755 index 0000000..59d67bb --- /dev/null +++ b/Model/NodeType/AbstractNode.php @@ -0,0 +1,128 @@ +_construct(); + $this->profiler = $profiler; + } + + /** + * Model construct that should be used for object initialization + * + * @return void + */ + protected function _construct() + { + } + + /** + * @inheritDoc + */ + public function fetchConfigData() + { + return []; + } + + /** + * @return string + */ + public function getResourceName() + { + return $this->_resourceName; + } + + /** + * @inheritDoc + */ + public function fetchData(array $nodes, $storeId) + { + $this->profiler->start(__METHOD__); + + $localNodes = []; + + foreach ($nodes as $node) { + $localNodes[$node->getId()] = $node; + } + + $this->profiler->stop(__METHOD__); + + return $localNodes; + } + + /** + * Standard model initialization + * + * @param string $resourceModel + * @return void + */ + protected function _init($resourceModel) + { + $this->_resourceName = $resourceModel; + } + + /** + * Get resource instance + * + * @throws LocalizedException + * @return \Ecomteck\Megamenu\Model\ResourceModel\NodeType\AbstractNode + */ + protected function getResource() + { + if (empty($this->_resourceName) && empty($this->_resource)) { + throw new LocalizedException( + new Phrase('The resource isn\'t set.') + ); + } + + return $this->_resource ?: ObjectManager::getInstance()->get($this->_resourceName); + } +} \ No newline at end of file diff --git a/Model/NodeType/Category.php b/Model/NodeType/Category.php new file mode 100755 index 0000000..b69ae06 --- /dev/null +++ b/Model/NodeType/Category.php @@ -0,0 +1,161 @@ +_init(\Ecomteck\Megamenu\Model\ResourceModel\NodeType\Category::class); + parent::_construct(); + } + + /** + * Category constructor. + * + * @param Profiler $profiler + * @param MetadataPool $metadataPool + * @param CollectionFactory $categoryCollection + */ + public function __construct( + Profiler $profiler, + MetadataPool $metadataPool, + CollectionFactory $categoryCollection + ) { + $this->metadataPool = $metadataPool; + $this->categoryCollection = $categoryCollection; + parent::__construct($profiler); + } + + /** + * {@inheritdoc} + * @throws \Exception + */ + public function fetchConfigData() + { + $this->profiler->start(__METHOD__); + $metadata = $this->metadataPool->getMetadata(CategoryInterface::class); + $identifierField = $metadata->getIdentifierField(); + + $data = $this->getResource()->fetchConfigData(); + $labels = []; + + foreach ($data as $row) { + if (isset($labels[$row['parent_id']])) { + $label = $labels[$row['parent_id']]; + } else { + $label = []; + } + $label[] = $row['name']; + $labels[$row[$identifierField]] = $label; + } + + $fieldOptions = []; + foreach ($labels as $id => $label) { + $fieldOptions[] = [ + 'label' => $label = implode(' > ', $label), + 'value' => $id + ]; + } + + $data = [ + 'ecomteckMenuAutoCompleteField' => [ + 'type' => 'category', + 'options' => $fieldOptions, + 'message' => __('Category not found'), + ], + ]; + + $this->profiler->stop(__METHOD__); + + return $data; + } + + /** + * @inheritDoc + */ + public function fetchData(array $nodes, $storeId) + { + $this->profiler->start(__METHOD__); + + $localNodes = []; + $categoryIds = []; + + foreach ($nodes as $node) { + $localNodes[$node->getId()] = $node; + $categoryIds[] = (int)$node->getContent(); + } + + $categoryUrls = $this->getResource()->fetchData($storeId, $categoryIds); + $categories = $this->getCategories($storeId, $categoryIds); + + $this->profiler->stop(__METHOD__); + + return [$localNodes, $categoryUrls, $categories]; + } + + /** + * @param $store + * @param array $categoryIds + * @return array + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function getCategories($store, array $categoryIds) + { + $return = []; + $categories = $this->categoryCollection->create() + ->addAttributeToSelect('*') + ->setStoreId($store) + ->addFieldToFilter( + 'entity_id', + ['in' => $categoryIds] + ); + + foreach ($categories as $category) { + $return[$category->getId()] = $category; + } + + return $return; + } +} diff --git a/Model/NodeType/CmsBlock.php b/Model/NodeType/CmsBlock.php new file mode 100755 index 0000000..6787069 --- /dev/null +++ b/Model/NodeType/CmsBlock.php @@ -0,0 +1,97 @@ +_init(\Ecomteck\Megamenu\Model\ResourceModel\NodeType\CmsBlock::class); + parent::_construct(); + } + + /** + * @inheritDoc + */ + public function fetchConfigData() + { + $this->profiler->start(__METHOD__); + + $options = $this->getResource()->fetchConfigData(); + + $fieldOptions = []; + + foreach ($options as $label => $value) { + $fieldOptions[] = [ + 'label' => $label, + 'value' => $value + ]; + } + + $data = [ + 'ecomteckMenuAutoCompleteField' => [ + 'type' => 'cms_block', + 'options' => $fieldOptions, + 'message' => __('CMS Block not found'), + ], + ]; + + $this->profiler->stop(__METHOD__); + + return $data; + } + + /** + * @inheritDoc + */ + public function fetchData(array $nodes, $storeId) + { + $this->profiler->start(__METHOD__); + + $localNodes = []; + $blocksCodes = []; + + foreach ($nodes as $node) { + $localNodes[$node->getId()] = $node; + $blocksCodes[] = $node->getContent(); + } + + $codes = $this->getResource()->fetchData($storeId, $blocksCodes); + + $content = []; + + foreach ($codes as $row) { + $content[$row['identifier']] = $row['content']; + } + + $this->profiler->stop(__METHOD__); + + return [$localNodes, $content]; + } +} diff --git a/Model/NodeType/CmsPage.php b/Model/NodeType/CmsPage.php new file mode 100755 index 0000000..6e8bd27 --- /dev/null +++ b/Model/NodeType/CmsPage.php @@ -0,0 +1,94 @@ +_init(\Ecomteck\Megamenu\Model\ResourceModel\NodeType\CmsPage::class); + parent::_construct(); + } + + /** + * @inheritDoc + */ + public function fetchConfigData() + { + $this->profiler->start(__METHOD__); + + $options = $this->getResource()->fetchConfigData(); + $fieldOptions = []; + + foreach ($options as $label => $value) { + $fieldOptions[] = [ + 'label' => $label, + 'value' => $value + ]; + } + + $data = [ + 'ecomteckMenuAutoCompleteField' => [ + 'type' => 'cms_page', + 'options' => $fieldOptions, + 'message' => __('CMS Page not found'), + ], + ]; + + $this->profiler->stop(__METHOD__); + + return $data; + } + + /** + * @inheritDoc + */ + public function fetchData(array $nodes, $storeId) + { + $this->profiler->start(__METHOD__); + + $localNodes = []; + $pagesCodes = []; + + foreach ($nodes as $node) { + $localNodes[$node->getId()] = $node; + $pagesCodes[] = $node->getContent(); + } + + /** @var \Ecomteck\Megamenu\Model\ResourceModel\NodeType\CmsPage $resource */ + $resource = $this->getResource(); + $pageIds = $resource->getPageIds($storeId, $pagesCodes); + $pageUrls = $resource->fetchData($storeId, $pageIds); + + + $this->profiler->stop(__METHOD__); + + return [$localNodes, $pageIds, $pageUrls]; + } +} diff --git a/Model/NodeType/CustomUrl.php b/Model/NodeType/CustomUrl.php new file mode 100755 index 0000000..4cf1fbd --- /dev/null +++ b/Model/NodeType/CustomUrl.php @@ -0,0 +1,30 @@ +_init(\Ecomteck\Megamenu\Model\ResourceModel\NodeType\Product::class); + parent::_construct(); + } + + public function __construct( + Profiler $profiler, + StoreManagerInterface $storeManager, + Session $customerSession + ) { + $this->storeManager = $storeManager; + $this->customerSession = $customerSession; + parent::__construct($profiler); + } + + /** + * @inheritDoc + */ + public function fetchData(array $nodes, $storeId) + { + $this->profiler->start(__METHOD__); + + $localNodes = []; + $productIds = []; + + $websiteId = $this->storeManager->getStore($storeId)->getWebsiteId(); + $customerGroupId = $this->customerSession->getCustomer()->getGroupId(); + + foreach ($nodes as $node) { + $localNodes[$node->getId()] = $node; + $productIds[] = (int)$node->getContent(); + } + + $resource = $this->getResource(); + $productImages = $resource->fetchImageData($storeId, $productIds); + $productUrls = $resource->fetchData($storeId, $productIds); + $productPrices = $resource->fetchPriceData($websiteId, $customerGroupId, $productIds); + $productTitles = $resource->fetchTitleData($storeId, $productIds); + $this->profiler->stop(__METHOD__); + + return [ + $localNodes, + $productUrls, + $productPrices, + $productImages, + $productTitles + ]; + } +} diff --git a/Model/NodeType/Wrapper.php b/Model/NodeType/Wrapper.php new file mode 100755 index 0000000..606cc12 --- /dev/null +++ b/Model/NodeType/Wrapper.php @@ -0,0 +1,30 @@ +providers = $providers; + } + + /** + * @param $type + * @param $nodes + */ + public function prepareData($type, $nodes) + { + $this->providers[$type]->fetchData($nodes); + } + + /** + * @param string $type + * @return \Ecomteck\Megamenu\Api\NodeTypeInterface + */ + public function getProvider($type) + { + return $this->providers[$type]; + } + + /** + * @param $type + * @param $id + * @param $level + * + * @return mixed + */ + public function render($type, $id, $level) + { + return $this->providers[$type]->getHtml($id, $level); + } + + /** + * @return array + */ + public function getLabels() + { + $result = []; + foreach ($this->providers as $code => $instance) { + $result[$code] = $instance->getLabel(); + } + return $result; + } + + /** + * @return array + */ + public function getEditForms() + { + return $this->providers; + } +} diff --git a/Model/ResourceModel/Menu.php b/Model/ResourceModel/Menu.php new file mode 100755 index 0000000..331d050 --- /dev/null +++ b/Model/ResourceModel/Menu.php @@ -0,0 +1,41 @@ +_init('ecomteck_menu', 'menu_id'); + } +} diff --git a/Model/ResourceModel/Menu/Collection.php b/Model/ResourceModel/Menu/Collection.php new file mode 100755 index 0000000..dcd3596 --- /dev/null +++ b/Model/ResourceModel/Menu/Collection.php @@ -0,0 +1,44 @@ +_init( + \Ecomteck\Megamenu\Model\Menu::class, + \Ecomteck\Megamenu\Model\ResourceModel\Menu::class + ); + } +} diff --git a/Model/ResourceModel/Menu/Node.php b/Model/ResourceModel/Menu/Node.php new file mode 100755 index 0000000..38efdb3 --- /dev/null +++ b/Model/ResourceModel/Menu/Node.php @@ -0,0 +1,41 @@ +_init('ecomteck_menu_node', 'node_id'); + } +} diff --git a/Model/ResourceModel/Menu/Node/Collection.php b/Model/ResourceModel/Menu/Node/Collection.php new file mode 100755 index 0000000..7a197e0 --- /dev/null +++ b/Model/ResourceModel/Menu/Node/Collection.php @@ -0,0 +1,44 @@ +_init( + \Ecomteck\Megamenu\Model\Menu\Node::class, + \Ecomteck\Megamenu\Model\ResourceModel\Menu\Node::class + ); + } +} diff --git a/Model/ResourceModel/NodeType/AbstractNode.php b/Model/ResourceModel/NodeType/AbstractNode.php new file mode 100755 index 0000000..6abaaf3 --- /dev/null +++ b/Model/ResourceModel/NodeType/AbstractNode.php @@ -0,0 +1,118 @@ +_resources = $resource; + parent::__construct(); + } + + /** + * Fetch additional data required for rendering nodes. + * + * @param int $storeId + * @param array $params + * + * @return mixed + */ + abstract public function fetchData($storeId = Store::DEFAULT_STORE_ID, $params = []); + + /** + * @inheritDoc + */ + abstract public function fetchConfigData(); + + /** + * Get real table name for db table, validated by db adapter + * + * @param string $tableName + * + * @return string + * @api + */ + public function getTable($tableName) + { + if (is_array($tableName)) { + list($tableName, $entitySuffix) = $tableName; + } else { + $entitySuffix = null; + } + + if ($entitySuffix !== null) { + $tableName .= '_' . $entitySuffix; + } + + if (!isset($this->_tables[$tableName])) { + $this->_tables[$tableName] = $this->_resources->getTableName( + $tableName, + ResourceConnection::DEFAULT_CONNECTION + ); + } + + return $this->_tables[$tableName]; + } + + /** + * Get connection + * + * @param string $resourceName + * + * @return \Magento\Framework\DB\Adapter\AdapterInterface + */ + public function getConnection($resourceName = ResourceConnection::DEFAULT_CONNECTION) + { + return $this->_resources->getConnection($resourceName); + } + + /** + * @inheritDoc + */ + protected function _construct() + { + } +} diff --git a/Model/ResourceModel/NodeType/Category.php b/Model/ResourceModel/NodeType/Category.php new file mode 100755 index 0000000..62edf7c --- /dev/null +++ b/Model/ResourceModel/NodeType/Category.php @@ -0,0 +1,115 @@ +metadataPool = $metadataPool; + parent::__construct($resource); + } + + /** + * @return array + * @throws \Exception + */ + public function fetchConfigData() + { + $metadata = $this->metadataPool->getMetadata(CategoryInterface::class); + $identifierField = $metadata->getIdentifierField(); + $linkField = $metadata->getLinkField(); + $connection = $this->getConnection('read'); + + $select = $connection->select()->from( + ['a' => $this->getTable('eav_attribute')], + ['attribute_id'] + )->join( + ['t' => $this->getTable('eav_entity_type')], + 't.entity_type_id = a.entity_type_id', + [] + )->where('t.entity_type_code = ?', CoreCategory::ENTITY)->where( + 'a.attribute_code = ?', + 'name' + ); + + $nameAttributeId = $connection->fetchOne($select); + + $select = $connection->select()->from( + ['e' => $this->getTable('catalog_category_entity')], + [$identifierField, 'parent_id'] + )->join( + ['v' => $this->getTable('catalog_category_entity_varchar')], + 'v.' . $linkField . ' = e.' . $linkField . ' AND v.store_id = 0 + AND v.attribute_id = ' . $nameAttributeId, + ['name' => 'v.value'] + )->where( + 'e.level > 0' + )->order( + 'e.level ASC' + )->order( + 'e.position ASC' + )->order( + 'e.' . $linkField . ' DESC' + ); + + return $connection->fetchAll($select); + } + + /** + * @param int $storeId + * @param array $categoryIds + * + * @return array + */ + public function fetchData($storeId = Store::DEFAULT_STORE_ID, $categoryIds = []) + { + $connection = $this->getConnection('read'); + $table = $this->getTable('url_rewrite'); + $select = $connection + ->select() + ->from($table, ['entity_id', 'request_path']) + ->where('entity_type = ?', 'category') + ->where('redirect_type = ?', 0) + ->where('store_id = ?', $storeId) + ->where('entity_id IN (' . implode(',', $categoryIds) . ')'); + + return $connection->fetchPairs($select); + } +} diff --git a/Model/ResourceModel/NodeType/CmsBlock.php b/Model/ResourceModel/NodeType/CmsBlock.php new file mode 100755 index 0000000..cfbf795 --- /dev/null +++ b/Model/ResourceModel/NodeType/CmsBlock.php @@ -0,0 +1,83 @@ +metadataPool = $metadataPool; + parent::__construct($resource); + } + + /** + * @return array + */ + public function fetchConfigData() + { + $connection = $this->getConnection('read'); + + $select = $connection->select()->from( + $this->getTable('cms_block'), + ['title', 'identifier'] + ); + + return $connection->fetchPairs($select); + } + + /** + * @param int $storeId + * @param array $blocksCodes + * @return array + * @throws \Exception + */ + public function fetchData($storeId = Store::DEFAULT_STORE_ID, $blocksCodes = []) + { + $linkField = $this->metadataPool->getMetadata(BlockInterface::class)->getLinkField(); + $connection = $this->getConnection('read'); + + $blockTable = $this->getTable('cms_block'); + $storeTable = $this->getTable('cms_block_store'); + + $select = $connection->select()->from( + ['p' => $blockTable], + ['content', 'identifier'] + )->join(['s' => $storeTable], 'p.' . $linkField . ' = s.' .$linkField, [])->where( + 's.store_id IN (0, ?)', + $storeId + )->where('p.identifier IN (?)', $blocksCodes)->where('p.is_active = ?', 1)->order('s.store_id ASC'); + + return $connection->fetchAll($select); + } +} diff --git a/Model/ResourceModel/NodeType/CmsPage.php b/Model/ResourceModel/NodeType/CmsPage.php new file mode 100755 index 0000000..6ac8c4f --- /dev/null +++ b/Model/ResourceModel/NodeType/CmsPage.php @@ -0,0 +1,166 @@ +metadataPool = $metadataPool; + $this->pageRepository = $pageRepository; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + parent::__construct($resource); + } + + /** + * @return array + */ + public function fetchConfigData() + { + $connection = $this->getConnection('read'); + + $select = $connection->select()->from( + $this->getTable('cms_page'), + ['title', 'identifier'] + ); + + return $connection->fetchPairs($select); + } + + /** + * @param int $storeId + * @param array $pageIds + * @return array + * @throws LocalizedException + */ + public function fetchData($storeId = Store::DEFAULT_STORE_ID, $pageIds = []) + { + $connection = $this->getConnection('read'); + $table = $this->getTable('url_rewrite'); + + $select = $connection + ->select() + ->from($table, ['entity_id', 'request_path']) + ->where('entity_type = ?', 'cms-page') + ->where('store_id = ?', $storeId) + ->where('entity_id IN (?)', array_values($pageIds)); + + $urlsBasedOnRewrites = $connection->fetchPairs($select); + + $additionalPageUrls = []; + $pageIdsWithMissingUrl = array_diff_key($pageIds, array_flip($urlsBasedOnRewrites)); + + $searchCriteria = $this->searchCriteriaBuilder + ->addFilter('page_id', $pageIdsWithMissingUrl, 'in') + ->addFilter('store_id', [$storeId, Store::DEFAULT_STORE_ID], 'in') + ->create(); + + $pages = $this->pageRepository->getList($searchCriteria); + + foreach ($pages->getItems() as $page) { + $additionalPageUrls[$page->getId()] = $page->getIdentifier(); + } + + return $urlsBasedOnRewrites + $additionalPageUrls; + } + + /** + * @param int|string $storeId + * @param array $pagesCodes + * @return array + * @throws \Exception + */ + public function getPageIds($storeId, $pagesCodes = []) + { + $metadata = $this->metadataPool->getMetadata(PageInterface::class); + $identifierField = $metadata->getIdentifierField(); + $linkField = $metadata->getLinkField(); + + $connection = $this->getConnection('read'); + + $pageTable = $this->getTable('cms_page'); + $storeTable = $this->getTable('cms_page_store'); + + $select = $connection->select()->from( + ['p' => $pageTable], + [$identifierField, 'identifier'] + )->join( + ['s' => $storeTable], + 'p.' . $linkField . ' = s.' . $linkField, + [] + )->where( + 's.store_id IN (0, ?)', + $storeId + )->where( + 'p.identifier IN (?)', + $pagesCodes + )->where( + 'p.is_active = ?', + 1 + )->order( + 's.store_id ASC' + ); + + $codes = $connection->fetchAll($select); + + $pageIds = []; + + foreach ($codes as $row) { + $pageIds[$row['identifier']] = $row[$identifierField]; + } + + return $pageIds; + } +} diff --git a/Model/ResourceModel/NodeType/Product.php b/Model/ResourceModel/NodeType/Product.php new file mode 100755 index 0000000..ae1744a --- /dev/null +++ b/Model/ResourceModel/NodeType/Product.php @@ -0,0 +1,145 @@ +productCollection = $productCollection; + $this->metadataPool = $metadataPool; + parent::__construct($resource); + } + + /** + * @param int $storeId + * @param array $productIds + * @return array + */ + public function fetchData($storeId = Store::DEFAULT_STORE_ID, $productIds = []) + { + $connection = $this->getConnection('read'); + $table = $this->getTable('url_rewrite'); + $select = $connection + ->select() + ->from($table, ['entity_id', 'request_path']) + ->where('entity_type = ?', 'product') + ->where('redirect_type = ?', 0) + ->where('store_id = ?', $storeId) + ->where('entity_id IN (?)', $productIds) + ->where('metadata IS NULL'); + + return $connection->fetchPairs($select); + } + + /** + * @param int $websiteId + * @param int $customerGroupId + * @param array $productIds + * @return array + */ + public function fetchPriceData($websiteId, $customerGroupId, $productIds = []) + { + $connection = $this->getConnection('read'); + $table = $this->getTable('catalog_product_index_price'); + $select = $connection + ->select() + ->from($table, ['entity_id', 'final_price']) + ->where('customer_group_id = ?', $customerGroupId) + ->where('website_id = ?', $websiteId) + ->where('entity_id IN (?)', $productIds); + + return $connection->fetchPairs($select); + } + + /** + * @param int $storeId + * @param array $productIds + * @return array + */ + public function fetchImageData($storeId, $productIds = []) + { + $collection = $this->productCollection->create(); + $collection->addAttributeToSelect(['thumbnail'], 'left') + ->addFieldToFilter('entity_id', ['in' => $productIds]) + ->addStoreFilter($storeId); + + $imageData = []; + foreach ($collection->getData() as $data) { + $imageData[$data['entity_id']] = $data['thumbnail'] ?? ''; + } + + return $imageData; + } + + /** + * @inheritDoc + */ + public function fetchConfigData() + { + return []; + } + + /** + * @param int $storeId + * @param array $productIds + * @return array + */ + public function fetchTitleData($storeId = Store::DEFAULT_STORE_ID, $productIds = []) + { + $collection = $this->productCollection->create(); + $collection->addAttributeToSelect(['name']) + ->addFieldToFilter('entity_id', ['in' => $productIds]) + ->addStoreFilter($storeId); + + $titleData = []; + foreach ($collection->getData() as $data) { + $titleData[$data['entity_id']] = $data['name'] ?? ''; + } + + return $titleData; + } +} diff --git a/Model/TemplateResolver.php b/Model/TemplateResolver.php new file mode 100755 index 0000000..3b4b2e8 --- /dev/null +++ b/Model/TemplateResolver.php @@ -0,0 +1,89 @@ +validator = $validator; + } + + /** + * @param Template $block + * @param string $menuId + * @param string $template + * @return string + */ + public function getMenuTemplate($block, $menuId, $template) + { + if (isset($this->templateMap[$menuId . '-' . $template])) { + return $this->templateMap[$menuId . '-' . $template]; + } + + $templateArr = explode('::', $template); + if (isset($templateArr[1])) { + $newTemplate = $templateArr[0] . '::' . $menuId . DIRECTORY_SEPARATOR . $templateArr[1]; + } else { + $newTemplate = $menuId . DIRECTORY_SEPARATOR . $template; + } + + if (!$this->validator->isValid($block->getTemplateFile($newTemplate))) { + return $this->setTemplateMap($menuId, $template, $template); + } + + return $this->setTemplateMap($menuId, $newTemplate, $template); + } + + /** + * @param string $menuId + * @param string $template + * @param string $oldTemplate + * @return string + */ + private function setTemplateMap($menuId, $template, $oldTemplate) + { + return $this->templateMap[$menuId . '-' . $oldTemplate] = $template; + } +} diff --git a/README.md b/README.md old mode 100644 new mode 100755 diff --git a/Setup/InstallData.php b/Setup/InstallData.php new file mode 100755 index 0000000..2d285c8 --- /dev/null +++ b/Setup/InstallData.php @@ -0,0 +1,37 @@ +startSetup(); + + $table = $installer->getConnection()->newTable( + $installer->getTable('ecomteck_menu') + )->addColumn( + 'menu_id', + Table::TYPE_INTEGER, + null, + ['identity' => true, 'nullable' => false, 'primary' => true, 'unsigned' => true,], + 'Menu ID' + )->addColumn( + 'title', + Table::TYPE_TEXT, + 255, + ['nullable' => false,], + 'Menu Title' + )->addColumn( + 'identifier', + Table::TYPE_TEXT, + 255, + ['nullable' => false], + 'Menu identifier' + )->addColumn( + 'creation_time', + Table::TYPE_TIMESTAMP, + null, + ['nullable' => false, 'default' => Table::TIMESTAMP_INIT,], + 'Creation Time' + )->addColumn( + 'update_time', + Table::TYPE_TIMESTAMP, + null, + ['nullable' => false, 'default' => Table::TIMESTAMP_INIT_UPDATE,], + 'Modification Time' + )->addColumn( + 'is_active', + Table::TYPE_SMALLINT, + null, + ['nullable' => false, 'default' => '1',], + 'Is Active' + ); + $installer->getConnection()->createTable($table); + + $table = $installer->getConnection()->newTable( + $installer->getTable('ecomteck_menu_node') + )->addColumn( + 'node_id', + Table::TYPE_INTEGER, + null, + ['identity' => true, 'nullable' => false, 'primary' => true, 'unsigned' => true,], + 'Node ID' + )->addColumn( + 'menu_id', + Table::TYPE_INTEGER, + null, + ['nullable' => false], + 'Menu ID' + )->addColumn( + 'type', + Table::TYPE_TEXT, + 255, + ['nullable' => false], + 'Node Type' + )->addColumn( + 'content', + Table::TYPE_TEXT, + null, + [], + 'Node contents' + )->addColumn( + 'classes', + Table::TYPE_TEXT, + 255, + [], + 'CSS class name' + )->addColumn( + 'parent_id', + Table::TYPE_INTEGER, + null, + ['unsigned' => true], + 'Parent Node ID' + )->addColumn( + 'position', + Table::TYPE_INTEGER, + null, + ['nullable' => false, 'unsigned' => true], + 'Node position' + )->addColumn( + 'level', + Table::TYPE_INTEGER, + null, + ['nullable' => false, 'unsigned' => true], + 'Node level' + )->addColumn( + 'title', + Table::TYPE_TEXT, + 255, + ['nullable' => false,], + 'Node Title' + )->addColumn( + 'creation_time', + Table::TYPE_TIMESTAMP, + null, + ['nullable' => false, 'default' => Table::TIMESTAMP_INIT,], + 'Creation Time' + )->addColumn( + 'update_time', + Table::TYPE_TIMESTAMP, + null, + ['nullable' => false, 'default' => Table::TIMESTAMP_INIT_UPDATE,], + 'Modification Time' + )->addColumn( + 'is_active', + Table::TYPE_SMALLINT, + null, + ['nullable' => false, 'default' => '1',], + 'Is Active' + ); + $installer->getConnection()->createTable($table); + + $table = $installer->getConnection()->newTable( + $installer->getTable('ecomteck_menu_store') + )->addColumn( + 'menu_id', + Table::TYPE_INTEGER, + null, + ['nullable' => false, 'primary' => true, 'unsigned' => true,], + 'Menu ID' + )->addColumn( + 'store_id', + Table::TYPE_INTEGER, + null, + ['nullable' => false, 'primary' => true, 'unsigned' => true,], + 'Store ID' + ); + $installer->getConnection()->createTable($table); + + $installer->endSetup(); + } +} diff --git a/Setup/UpgradeSchema.php b/Setup/UpgradeSchema.php new file mode 100755 index 0000000..b9f4918 --- /dev/null +++ b/Setup/UpgradeSchema.php @@ -0,0 +1,203 @@ +startSetup(); + + if (version_compare($context->getVersion(), '1.0.1', '<')) { + $this->changeTitleType($setup); + $this->addMenuCssClassField($setup); + $this->addTargetAttribute($setup); + $this->updateTargetAttribute($setup); + $this->addForeignKeys($setup); + } + + $setup->endSetup(); + } + + /** + * @param SchemaSetupInterface $setup + * @return $this + */ + private function addMenuCssClassField(SchemaSetupInterface $setup) + { + $setup->getConnection()->addColumn( + $setup->getTable('ecomteck_menu'), + 'css_class', + [ + 'type' => Table::TYPE_TEXT, + 'length' => 255, + 'nullable' => true, + 'after' => 'identifier', + 'default' => 'menu', + 'comment' => 'CSS Class' + ] + ); + + return $this; + } + + private function changeTitleType(SchemaSetupInterface $setup) + { + $setup->getConnection()->modifyColumn( + $setup->getTable('ecomteck_menu_node'), + 'title', + [ + 'type' => Table::TYPE_TEXT, + 'nullable' => false + ], + 'Demo Title' + ); + } + + private function addTargetAttribute(SchemaSetupInterface $setup) + { + $setup->getConnection()->addColumn( + $setup->getTable('ecomteck_menu_node'), + 'target', + [ + 'type' => Table::TYPE_TEXT, + 'length' => 10, + 'nullable' => true, + 'after' => 'title', + 'default' => '_self', + 'comment' => 'Link target', + ] + ); + + return $this; + } + + private function updateTargetAttribute(SchemaSetupInterface $setup) + { + $table = $setup->getTable('ecomteck_menu_node'); + $connection = $setup->getConnection(); + + $connection->update( + $table, + ['target' => 0], + "target = '_self'" + ); + $connection->update( + $table, + ['target' => 1], + "target = '_blank'" + ); + $connection->modifyColumn( + $table, + 'target', + [ + 'type' => Table::TYPE_BOOLEAN, + 'default' => 0, + ] + ); + } + + private function addForeignKeys(SchemaSetupInterface $setup) + { + $menuTable = $setup->getTable('ecomteck_menu'); + $nodeTable = $setup->getTable('ecomteck_menu_node'); + $storeTable = $setup->getTable('ecomteck_menu_store'); + $setup->getConnection()->modifyColumn( + $nodeTable, + 'menu_id', + [ + 'type' => Table::TYPE_INTEGER, + 'length' => 10, + 'nullable' => false, + 'unsigned' => true, + 'comment' => 'Menu ID' + ] + ); + + $setup->getConnection()->modifyColumn( + $storeTable, + 'store_id', + [ + 'type' => Table::TYPE_SMALLINT, + 'length' => 5, + 'nullable' => false, + 'primary' => true, + 'unsigned' => true, + 'comment' => 'Store ID' + ] + ); + + $setup->getConnection()->addForeignKey( + $setup->getFkName( + 'ecomteck_menu_node', + 'menu_id', + 'ecomteck_menu', + 'menu_id' + ), + $nodeTable, + 'menu_id', + $menuTable, + 'menu_id', + Table::ACTION_CASCADE + ); + + $setup->getConnection()->addForeignKey( + $setup->getFkName( + 'ecomteck_menu_store', + 'menu_id', + 'ecomteck_menu', + 'menu_id' + ), + $storeTable, + 'menu_id', + $menuTable, + 'menu_id', + Table::ACTION_CASCADE + ); + + $setup->getConnection()->addForeignKey( + $setup->getFkName( + 'ecomteck_menu_store', + 'store_id', + 'store', + 'store_id' + ), + $storeTable, + 'store_id', + $setup->getTable('store'), + 'store_id', + Table::ACTION_CASCADE + ); + } +} diff --git a/Ui/Component/Listing/Column/MenuList/MenuActions.php b/Ui/Component/Listing/Column/MenuList/MenuActions.php new file mode 100755 index 0000000..ca20daf --- /dev/null +++ b/Ui/Component/Listing/Column/MenuList/MenuActions.php @@ -0,0 +1,57 @@ +getData("name"); + $id = "X"; + if (isset($item["menu_id"])) { + $id = $item["menu_id"]; + } + $item[$name]["view"] = [ + "href" => $this->getContext()->getUrl( + "megamenu/menu/edit", + ["id" => $id] + ), + "label" => __("Edit"), + ]; + } + } + + return $dataSource; + } +} diff --git a/Ui/Component/Listing/DataProviders/EcomteckMenu/Menu/ListProvider.php b/Ui/Component/Listing/DataProviders/EcomteckMenu/Menu/ListProvider.php new file mode 100755 index 0000000..ec0aecb --- /dev/null +++ b/Ui/Component/Listing/DataProviders/EcomteckMenu/Menu/ListProvider.php @@ -0,0 +1,53 @@ +collection = $collectionFactory->create(); + } +} diff --git a/composer.json b/composer.json old mode 100644 new mode 100755 diff --git a/etc/acl.xml b/etc/acl.xml new file mode 100755 index 0000000..7b70254 --- /dev/null +++ b/etc/acl.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + diff --git a/etc/adminhtml/menu.xml b/etc/adminhtml/menu.xml new file mode 100755 index 0000000..d57bbbc --- /dev/null +++ b/etc/adminhtml/menu.xml @@ -0,0 +1,27 @@ + + + + + + + diff --git a/etc/adminhtml/routes.xml b/etc/adminhtml/routes.xml new file mode 100755 index 0000000..0e7d45a --- /dev/null +++ b/etc/adminhtml/routes.xml @@ -0,0 +1,30 @@ + + + + + + + + + + diff --git a/etc/di.xml b/etc/di.xml new file mode 100755 index 0000000..f1eac4f --- /dev/null +++ b/etc/di.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + Ecomteck\Megamenu\Block\NodeType\Category + Ecomteck\Megamenu\Block\NodeType\Product + Ecomteck\Megamenu\Block\NodeType\CmsPage + Ecomteck\Megamenu\Block\NodeType\CmsBlock + Ecomteck\Megamenu\Block\NodeType\CustomUrl + Ecomteck\Megamenu\Block\NodeType\Wrapper + + + + \ No newline at end of file diff --git a/etc/module.xml b/etc/module.xml old mode 100644 new mode 100755 index b5649d3..edf94a7 --- a/etc/module.xml +++ b/etc/module.xml @@ -1,4 +1,26 @@ + + - + diff --git a/etc/webapi.xml b/etc/webapi.xml new file mode 100755 index 0000000..1c2434c --- /dev/null +++ b/etc/webapi.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/registration.php b/registration.php old mode 100644 new mode 100755 index 11139a0..46c662f --- a/registration.php +++ b/registration.php @@ -1,6 +1,26 @@ + + + + diff --git a/view/adminhtml/layout/megamenu_menu_edit.xml b/view/adminhtml/layout/megamenu_menu_edit.xml new file mode 100755 index 0000000..bbc9f4f --- /dev/null +++ b/view/adminhtml/layout/megamenu_menu_edit.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + main_section + megamenu_edit_tab_main + + + nodes + megamenu_edit_tab_node + + + + + diff --git a/view/adminhtml/layout/megamenu_menu_index.xml b/view/adminhtml/layout/megamenu_menu_index.xml new file mode 100755 index 0000000..749a0ea --- /dev/null +++ b/view/adminhtml/layout/megamenu_menu_index.xml @@ -0,0 +1,31 @@ + + + + + + + + + diff --git a/view/adminhtml/requirejs-config.js b/view/adminhtml/requirejs-config.js new file mode 100755 index 0000000..93a0f57 --- /dev/null +++ b/view/adminhtml/requirejs-config.js @@ -0,0 +1,11 @@ +var config = { + paths: { + "Vue": "Ecomteck_Megamenu/js/lib/vue", + "vue": "Ecomteck_Megamenu/js/lib/require-vuejs", + "Vddl": "Ecomteck_Megamenu/js/lib/vddl", + "vue-select": "Ecomteck_Megamenu/js/lib/vue-select" + }, + shim: { + "Vue": { "exports": "Vue" } + } +}; diff --git a/view/adminhtml/templates/menu/nodes.phtml b/view/adminhtml/templates/menu/nodes.phtml new file mode 100755 index 0000000..b06b4d9 --- /dev/null +++ b/view/adminhtml/templates/menu/nodes.phtml @@ -0,0 +1,81 @@ +getNodeForms() as $form) { + $fieldData[$form->getNodeType()] = $form->getJsonConfig(); +} +?> + +
    + +
    +
    + + + + + + + + +
    +
    +
    +
    + + diff --git a/view/adminhtml/ui_component/megamenu_menu_list.xml b/view/adminhtml/ui_component/megamenu_menu_list.xml new file mode 100755 index 0000000..389c340 --- /dev/null +++ b/view/adminhtml/ui_component/megamenu_menu_list.xml @@ -0,0 +1,122 @@ + + + ++ + + megamenu_menu_list.megamenu_menu_list_data_source + megamenu_menu_list.megamenu_menu_list_data_source + + megamenu_menu_list_columns + + + add + Create new menu + primary + */*/create + + + + + + Ecomteck\Megamenu\Ui\Component\Listing\DataProviders\EcomteckMenu\Menu\ListProvider + megamenu_menu_list_data_source + menu_id + id + + + + + menu_id + + + + + + + Magento_Ui/js/grid/provider + + + + + + + + false + 55 + menu_id + + + + + + + textRange + asc + ID + + + + + + + textRange + asc + Title + + + + + + + textRange + asc + Identifier + + + + + + + + false + 107 + menu_id + + + + + + + + diff --git a/view/adminhtml/web/css/source/_module.less b/view/adminhtml/web/css/source/_module.less new file mode 100755 index 0000000..9468cc0 --- /dev/null +++ b/view/adminhtml/web/css/source/_module.less @@ -0,0 +1,5 @@ +@import './blocks/_var.less'; +@import './blocks/_vddl-base.less'; +@import './blocks/_panel.less'; +@import './blocks/_nested-list.less'; +@import './blocks/_selected-option.less'; diff --git a/view/adminhtml/web/css/source/blocks/_nested-list.less b/view/adminhtml/web/css/source/blocks/_nested-list.less new file mode 100755 index 0000000..065f15a --- /dev/null +++ b/view/adminhtml/web/css/source/blocks/_nested-list.less @@ -0,0 +1,15 @@ +.vddl-placeholder { + width: 100%; + padding: 0 15px; + box-sizing: border-box; + + &__inner { + width: 100%; + height: @ecomteck-menu__item-height; + line-height: @ecomteck-menu__item-height; + background: #f5f5f5; + border-radius: 8px; + border: 1px solid #eee; + margin: 15px 0; + } +} diff --git a/view/adminhtml/web/css/source/blocks/_panel.less b/view/adminhtml/web/css/source/blocks/_panel.less new file mode 100755 index 0000000..9ec8c84 --- /dev/null +++ b/view/adminhtml/web/css/source/blocks/_panel.less @@ -0,0 +1,162 @@ +.panel { + overflow: hidden; + border-radius: 8px; + border: 1px solid @color-gray90; + margin-bottom: 15px; + + &--open { + overflow: visible; + } + + &__heading { + display: flex; + justify-content: space-between; + flex-wrap: wrap; + align-items: flex-start; + height: @ecomteck-menu__item-height; + line-height: @ecomteck-menu__item-height; + padding: 0 10px 0 0; + border-radius: 7px 7px 0 0; + border-bottom: 1px solid @color-gray90; + margin-bottom: -1px; + + &:hover { + background-color: @color-white-fog; + } + } + &__body { + min-height: @ecomteck-menu__item-height; + + > .panel { + margin: 10px; + } + + &--item, + .panel__placeholder { + width: 100%; + min-height: @ecomteck-menu__item-height; + line-height: @ecomteck-menu__item-height; + padding: 0 15px; + box-sizing: border-box; + } + + &--item.no-padding-left { + padding-left: 0; + } + + &--item:last-child { + border-bottom: none; + } + + &--item { + > .panel { + margin: 15px 0; + } + } + } + + &__heading-text { + margin-left: 0; + margin-right: auto; + cursor: pointer; + flex-grow: 1; + } + + &__heading-type { + font-size: 12px; + color: @color-gray52; + padding-left: 5px; + } + + &__buttom { + border-color: transparent; + color: @primary__color; + background-color: transparent; + padding: 0; + margin: 0 5px; + border: none; + + &:focus, + &:active { + color: @primary__color; + background-color: transparent; + } + + &:hover { + color: @primary__color__darker; + background-color: transparent; + } + + &:before { + &:extend(.abs-icon all); + line-height: @ecomteck-menu__item-height; + vertical-align: middle; + font-size: 14px; + } + + &--append { + &:before { + content: @icon-plus__content; + } + } + + &--edit { + &:before { + content: @icon-edit__content; + } + } + + &--delete { + &:before { + content: @icon-delete__content; + } + } + } + + &__collapse { + height: @ecomteck-menu__item-height; + cursor: pointer; + width: 24px; + text-align: center; + + &:before { + &:extend(.abs-icon all); + line-height: @ecomteck-menu__item-height; + vertical-align: middle; + font-size: 14px; + color: @color-gray-darken3; + } + + &--down { + &:before { + content: @icon-expand-close__content; + } + } + + &--up { + &:before { + content: @icon-expand-open__content; + } + } + &--none { + &:before { + content: ''; + } + } + } + + &__empty-text { + text-align: center; + color: @color-gray-darken2; + line-height: @ecomteck-menu__item-height; + } + + &__loader { + text-align: center; + } + + .admin__fieldset { + padding-bottom: 0; + border-bottom: 1px solid #eee; + } +} diff --git a/view/adminhtml/web/css/source/blocks/_selected-option.less b/view/adminhtml/web/css/source/blocks/_selected-option.less new file mode 100755 index 0000000..151d82d --- /dev/null +++ b/view/adminhtml/web/css/source/blocks/_selected-option.less @@ -0,0 +1,36 @@ +.selected-option { + width: 100%; + clear: both; + padding: 10px 0; + + &::after { + content: ''; + display: block; + clear: both; + } + + &__label { + float: left; + width: 20%; + color: #303030; + font-size: 14px; + font-weight: 600; + line-height: 3.2rem; + padding: 0 30px 0 0; + white-space: nowrap; + word-wrap: break-word; + text-align: right; + } + + &__value { + float: left; + width: 60%; + margin: 0; + line-height: 3.2rem; + font-size: 14px; + } +} + +.v-select.unsearchable input[type=search] { + opacity: 1 !important; +} diff --git a/view/adminhtml/web/css/source/blocks/_var.less b/view/adminhtml/web/css/source/blocks/_var.less new file mode 100755 index 0000000..ee30a47 --- /dev/null +++ b/view/adminhtml/web/css/source/blocks/_var.less @@ -0,0 +1 @@ +@ecomteck-menu__item-height: 30px; diff --git a/view/adminhtml/web/css/source/blocks/_vddl-base.less b/view/adminhtml/web/css/source/blocks/_vddl-base.less new file mode 100755 index 0000000..6caedc3 --- /dev/null +++ b/view/adminhtml/web/css/source/blocks/_vddl-base.less @@ -0,0 +1,31 @@ +/** + * For the correct positioning of the placeholder element, the vddl-list and + * it's children must have position: relative + */ +.vddl-list, +.vddl-draggable { + position: relative; +} + +/** + * The vddl-list should always have a min-height, + * otherwise you can't drop to it once it's empty + */ +.vddl-list { + padding-left: 0px; + min-height: @ecomteck-menu__item-height; +} + +.vddl-dragging { + opacity: 0.7; +} + +/** + * The vddl-dragging-source class will be applied to + * the source element of a drag operation. It makes + * sense to hide it to give the user the feeling + * that he's actually moving it. + */ +.vddl-dragging-source { + display: none; +} diff --git a/view/adminhtml/web/js/lib/require-vuejs.js b/view/adminhtml/web/js/lib/require-vuejs.js new file mode 100755 index 0000000..5a36372 --- /dev/null +++ b/view/adminhtml/web/js/lib/require-vuejs.js @@ -0,0 +1,267 @@ +(function() { + /* jshint ignore:start */ + + /* jshint ignore:end */ + + define("require_vuejs", function(){ + return plugin; + }); + /*vim: set ts=4 ex=4 tabshift=4 expandtab :*/ + + /*globals: define, require */ + /* + * css-parser.js + * + * Distributed under terms of the MIT license. + */ + /* jshint ignore:start */ + + /* jshint ignore:end */ + + var css_parser = (function(){ + "use strict"; + var extractCss = function(text) { + var start = text.indexOf(""); + + if( start === -1 ) { + return false; + } else { + return text.substring(start + 7, end); + } + }; + + var appendCSSStyle = function(css) { + if(css && typeof document !== "undefined") { + var style = document.createElement("style"); + var head = document.head || document.getElementsByTagName("head")[0]; + + style.type = "text/css"; + if (style.styleSheet){ + style.styleSheet.cssText = css; + } else { + style.appendChild(document.createTextNode(css)); + } + + head.appendChild(style); + } + }; + + var createDocumentMock = function() { + return { + createElement: function() {}, + head: {}, + getElementsByTagName: function() {}, + createTextNode: function() {} + }; + }; + + return { + extractCss: extractCss, + appendCSSStyle: appendCSSStyle, + functionString: function(text) { + if (typeof document === "undefined") // you are running optimization ( r.js ) + var document = createDocumentMock(); // var put it on start of scope + + var css = extractCss(text); + if ( css === false ) { + return ""; + } else { + css = css + .replace(/([^\\])'/g, "$1\\'") + .replace(/[\n\r]+/g, "") + .replace(/ {2,20}/g, " "); + } + + var result = "(" + appendCSSStyle.toString() + ")('" + css + "');"; + return result; + }, + parse: function(text) { + var css = extractCss(text); + appendCSSStyle(css); + } + }; + })(); + + /* + * template-parser.js + * + * Distributed under terms of the MIT license. + */ + /* jshint ignore:start */ + + /* jshint ignore:end */ + + var template_parser = (function(){ + + var extractTemplate = function(text) { + var start = text.indexOf(""); + return text.substring(start + 10, end) + .replace(/([^\\])'/g, "$1\\'") + .replace(/^(.*)$/mg, "'$1' + ") // encapsulate template code between ' and put a + + .replace(/ {2,20}/g, " ") + "''"; + }; + + + return { + extractTemplate: extractTemplate + }; + + })(); + + /* + * script-parser.js + * Copyright (C) 2017 Edgard Leal + * + * Distributed under terms of the MIT license. + */ + + /* jshint ignore:start */ + + /* jshint ignore:end */ + + var script_parser = (function(){ + return { + findCloseTag: function(text, start) { + var i = start; + while(i < text.length && text[i++] !== ">"); + return i; + }, + extractScript: function(text) { + var start = text.indexOf(""); + return text.substring(sizeOfStartTag, end); + } + }; + })(); + + /* + * vue.js + * + * Distributed under terms of the MIT license. + */ + /* global Promise */ + /* jshint ignore:start */ + + /* jshint ignore:end */ + + var plugin = (function(){ + "use strict"; + + var modulesLoaded = {}; + + var functionTemplate = ["(function(template){", "})("]; + + var parse = function(text) { + var template = template_parser.extractTemplate(text); + var source = script_parser.extractScript(text); + var functionString = css_parser.functionString(text); + + return functionTemplate[0] + + source + + functionString + + functionTemplate[1] + + template + ");"; + }; + + var loadLocal = function(url, name) { + var fs = require.nodeRequire("fs"); + var text = fs.readFileSync(url, "utf-8"); + if(text[0] === "\uFEFF") { // remove BOM ( Byte Mark Order ) from utf8 files + text = text.substring(1); + } + var parsed = parse(text).replace(/(define\()\s*(\[.*)/, "$1\"vue!" + name + "\", $2"); + return parsed; + }; + + return { + normalize: function(name, normalize) { + return normalize(name); + }, + write: function(pluginName, moduleName, write) { + write.asModule(pluginName + "!" + moduleName, modulesLoaded[moduleName]); + }, + load: function (name, req, onload, config) { + var url, extension; + + if (config.paths && config.paths[name]) { + name = config.paths[name]; + } + + // if file name has an extension, don't add .vue + if(/.*(\.vue)|(\.html?)/.test(name)) { + extension = ""; + } else { + extension = ".vue"; + } + + url = req.toUrl(name + extension); + + // this is used to browser to create a way to debug the file + var sourceHeader = config.isBuild?"" : "//# sourceURL=" + location.origin + url + "\n"; + var loadRemote; + + if(config.isBuild) { + loadRemote = function(url, callback) { + return new Promise(function(resolve, reject) { + try { + var fs = require.nodeRequire("fs"); + var text = fs.readFileSync(url, "utf-8").toString(); + if(text[0] === "\uFEFF") { // remove BOM ( Byte Mark Order ) from utf8 files + text = text.substring(1); + } + var parsed = parse(text).replace(/(define\()\s*(\[.*)/, "$1\"" + name + "\", $2"); + callback(parsed); + resolve(parsed); + } catch(error) { + reject(error); + } + }); + }; + } else { + loadRemote = function(path, callback) { + var xhttp = new XMLHttpRequest(); + xhttp.onreadystatechange = function() { + if (this.readyState === 4 && (this.status === 200 || this.status === 304)) { + callback(parse(xhttp.responseText)); + } + }; + xhttp.open("GET", path, true); + xhttp.send(); + }; + } + + req([], function() { + if(config.isBuild) { + var data = loadLocal(url, name); + modulesLoaded[name] = data; + onload.fromText(data); + } else { + loadRemote(url, function(text){ + modulesLoaded[name] = sourceHeader + text; + onload.fromText(modulesLoaded[name]); + }); + } + }); + } + }; + })(); + + /** + * vue.js + * Copyright (C) 2017 + * + * Distributed under terms of the MIT license. + */ + /* jshint ignore:start */ + + /* jshint ignore:end */ + + define("vue", function(){ + return plugin; + }); + /* vim: set tabstop=4 softtabstop=4 shiftwidth=4 expandtab : */ + +})(); diff --git a/view/adminhtml/web/js/lib/vddl.js b/view/adminhtml/web/js/lib/vddl.js new file mode 100755 index 0000000..94eca2c --- /dev/null +++ b/view/adminhtml/web/js/lib/vddl.js @@ -0,0 +1,498 @@ +/*! + * Vddl.js v0.7.1 + * (c) 2017 Hejx + * Released under the MIT License. + * https://github.com/hejianxian/vddl#readme + */ + +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global.DragAndDropList = factory()); +}(this, (function () { + +var Draggable = { template: "
    ", + name: 'vddl-draggable', + // css: vddl-dragging, vddl-dragging-source + props: { + draggable: [ Object, Array ], + wrapper: Array, + index: Number, + + effectAllowed: String, + type: String, + + // diable + disableIf: Boolean, + + // callback fn + dragstart: Function, + selected: Function, + dragend: Function, + moved: Function, + copied: Function, + canceled: Function, + }, + data: function data() { + return {}; + }, + computed: {}, + methods: { + handleDragstart: function handleDragstart(event) { + var this$1 = this; + + var draggable = JSON.stringify(this.draggable); + // Check whether the element is draggable, since dragstart might be triggered on a child. + if (draggable == 'false' || this.disableIf) { return true; } + + // Serialize the data associated with this element. IE only supports the Text drag type + event.dataTransfer.setData("Text", draggable); + + // Only allow actions specified in effect-allowed attribute + event.dataTransfer.effectAllowed = this.effectAllowed || "move"; + + // Add CSS classes. IE9 not support 'classList' + this.$el.className = this.$el.className.trim() + " vddl-dragging"; + setTimeout(function () { + this$1.$el.className = this$1.$el.className.trim() + " vddl-dragging-source"; + }, 0); + + // Workarounds for stupid browsers, see description below + this.vddlDropEffectWorkaround.dropEffect = "none"; + this.vddlDragTypeWorkaround.isDragging = true; + + // Save type of item in global state. Usually, this would go into the dataTransfer + // typename, but we have to use "Text" there to support IE + this.vddlDragTypeWorkaround.dragType = this.type || undefined; + + // Try setting a proper drag image if triggered on a vddl-handle (won't work in IE). + if (event._dndHandle && event.dataTransfer.setDragImage) { + event.dataTransfer.setDragImage(this.$el, event._dndHandleLeft, event._dndHandleTop); + } + + // Invoke callback + if (typeof(this.dragstart) === 'function') { + this.dragstart.call(this, event.target); + } + }, + + handleDragend: function handleDragend(event) { + var this$1 = this; + + var dropEffect = this.vddlDropEffectWorkaround.dropEffect; + switch (dropEffect) { + case "move": + if (typeof(this.moved) === 'function') { + this.$nextTick(function () { + this$1.moved({ + index: this$1.index, + list: this$1.wrapper, + event: event.target, + draggable: this$1.draggable, + }); + }); + } else { + this.$nextTick(function () { + this$1.wrapper.splice(this$1.index, 1); + }); + } + break; + case "copy": + if (typeof(this.copied) === 'function') { + this.copied(this.draggable, event.target); + } + break; + case "none": + if (typeof(this.canceled) === 'function') { + this.canceled(event.target); + } + break; + } + if (typeof(this.dragend) === 'function') { + this.dragend(dropEffect, event.target); + } + + // Clean up + this.$el.className = this.$el.className.replace("vddl-dragging", "").trim(); + setTimeout(function () { + if (this$1.$el) { this$1.$el.className = this$1.$el.className.replace("vddl-dragging-source", "").trim(); } + }, 0); + this.vddlDragTypeWorkaround.isDragging = false; + }, + + handleClick: function handleClick(event) { + if (!this.selected) { return; } + + if (typeof(this.selected) === 'function') { + this.selected(this.wrapper[this.index], event.target); + } + }, + + /** + * Workaround to make element draggable in IE9 + * http://stackoverflow.com/questions/5500615/internet-explorer-9-drag-and-drop-dnd + */ + handleSelected: function handleSelected() { + if (this.dragDrop) { this.dragDrop(); } + return false; + }, + + // init + init: function init() { + var status = true; + if (this.disableIf) { status = false; } + this.$el.setAttribute('draggable', status); + }, + }, + watch: { + disableIf: function disableIf(val) { + this.$el.setAttribute('draggable', !val); + }, + }, + // For Vue 1.0 + ready: function ready() { + this.init(); + }, + mounted: function mounted() { + this.init(); + }, +}; + +var List = { template: "
    ", + name: 'vddl-list', + // css: placeholder, dragover + props: { + list: Array, + + allowedTypes: Array, + disableIf: Boolean, + horizontal: Boolean, + externalSources: Boolean, + + dragover: Function, + inserted: Function, + drop: Function, + }, + data: function data() { + return {}; + }, + computed: {}, + methods: { + handleDragenter: function handleDragenter(event) { + if (!this.isDropAllowed(event)) { return true; } + }, + + handleDragover: function handleDragover(event) { + var this$1 = this; + + if (!this.isDropAllowed(event)) { return true; } + + if (this.placeholderNode.parentNode != this.listNode) { + this.listNode.appendChild(this.placeholderNode); + } + + if (event.target !== this.listNode) { + // Try to find the node direct directly below the list node. + var listItemNode = event.target; + while (listItemNode.parentNode !== this.listNode && listItemNode.parentNode) { + listItemNode = listItemNode.parentNode; + } + if (listItemNode.parentNode === this.listNode && listItemNode !== this.placeholderNode) { + // If the mouse pointer is in the upper half of the child element, + // we place it before the child element, otherwise below it. + if (this.isMouseInFirstHalf(event, listItemNode)) { + this.listNode.insertBefore(this.placeholderNode, listItemNode); + } else { + this.listNode.insertBefore(this.placeholderNode, listItemNode.nextSibling); + } + } + } else { + // This branch is reached when we are dragging directly over the list element. + // Usually we wouldn't need to do anything here, but the IE does not fire it's + // events for the child element, only for the list directly. Therefore, we repeat + // the positioning algorithm for IE here. + if (this.isMouseInFirstHalf(event, this.placeholderNode, true)) { + // Check if we should move the placeholder element one spot towards the top. + // Note that display none elements will have offsetTop and offsetHeight set to + // zero, therefore we need a special check for them. + while (this.placeholderNode.previousElementSibling + && (this.isMouseInFirstHalf(event, this.placeholderNode.previousElementSibling, true) + || this.placeholderNode.previousElementSibling.offsetHeight === 0)) { + this$1.listNode.insertBefore(this$1.placeholderNode, this$1.placeholderNode.previousElementSibling); + } + } else { + // Check if we should move the placeholder element one spot towards the bottom + while (this.placeholderNode.nextElementSibling && + !this.isMouseInFirstHalf(event, this.placeholderNode.nextElementSibling, true)) { + this$1.listNode.insertBefore(this$1.placeholderNode, + this$1.placeholderNode.nextElementSibling.nextElementSibling); + } + } + } + + // At this point we invoke the callback, which still can disallow the drop. + // We can't do this earlier because we want to pass the index of the placeholder. + if (this.dragover && !this.invokeCallback('dragover', event, this.getPlaceholderIndex())) { + return this.stopDragover(event); + } + + if (this.$el.className.indexOf("vddl-dragover") < 0) { this.$el.className = this.$el.className.trim() + " vddl-dragover"; } + return false; + }, + handleDrop: function handleDrop(event) { + if (!this.isDropAllowed(event)) { return true; } + + // The default behavior in Firefox is to interpret the dropped element as URL and + // forward to it. We want to prevent that even if our drop is aborted. + + // Unserialize the data that was serialized in dragstart. According to the HTML5 specs, + // the "Text" drag type will be converted to text/plain, but IE does not do that. + var data = event.dataTransfer.getData("Text") || event.dataTransfer.getData("text/plain"); + var transferredObject; + try { + transferredObject = JSON.parse(data); + } catch(e) { + return this.stopDragover(); + } + + // Invoke the callback, which can transform the transferredObject and even abort the drop. + var index = this.getPlaceholderIndex(); + if (this.drop) { + transferredObject = this.invokeCallback('drop', event, index, transferredObject); + if (!transferredObject) { + return this.stopDragover(); + } + } + + // Insert the object into the array, unless drop took care of that (returned true). + if (transferredObject !== true) { + this.list.splice(index, 0, transferredObject); + } + this.invokeCallback('inserted', event, index, transferredObject); + + // In Chrome on Windows the dropEffect will always be none... + // We have to determine the actual effect manually from the allowed effects + if (event.dataTransfer.dropEffect === "none") { + if (event.dataTransfer.effectAllowed === "copy" || + event.dataTransfer.effectAllowed === "move") { + this.vddlDropEffectWorkaround.dropEffect = event.dataTransfer.effectAllowed; + } else { + this.vddlDropEffectWorkaround.dropEffect = event.ctrlKey ? "copy" : "move"; + } + } else { + this.vddlDropEffectWorkaround.dropEffect = event.dataTransfer.dropEffect; + } + + // Clean up + this.stopDragover(); + return false; + }, + handleDragleave: function handleDragleave(event) { + var this$1 = this; + + this.$el.className = this.$el.className.replace("vddl-dragover", "").trim(); + setTimeout(function () { + if (this$1.$el.className.indexOf("vddl-dragover") < 0) { + this$1.placeholderNode.parentNode && this$1.placeholderNode.parentNode.removeChild(this$1.placeholderNode); + } + }, 100); + }, + + // Checks whether the mouse pointer is in the first half of the given target element. + isMouseInFirstHalf: function isMouseInFirstHalf(event, targetNode, relativeToParent) { + var mousePointer = this.horizontal ? (event.offsetX || event.layerX) + : (event.offsetY || event.layerY); + var targetSize = this.horizontal ? targetNode.offsetWidth : targetNode.offsetHeight; + var targetPosition = this.horizontal ? targetNode.offsetLeft : targetNode.offsetTop; + targetPosition = relativeToParent ? targetPosition : 0; + return mousePointer < targetPosition + targetSize / 2; + }, + + /** + * Tries to find a child element that has the 'vddl-placeholder' class set. If none was found, a + * new div element is created. + */ + getPlaceholderElement: function getPlaceholderElement() { + var placeholder, + oldPlaceholder = this.$el.parentNode.querySelectorAll('.vddl-placeholder'); + if (oldPlaceholder.length > 0) { + placeholder = oldPlaceholder[0]; + return placeholder; + } + var newPlaceholder = document.createElement('div'); + newPlaceholder.setAttribute('class', 'vddl-placeholder'); + return newPlaceholder; + }, + + getPlaceholderIndex: function getPlaceholderIndex() { + return Array.prototype.indexOf.call(this.listNode.children, this.placeholderNode); + }, + + /** + * Checks various conditions that must be fulfilled for a drop to be allowed + */ + isDropAllowed: function isDropAllowed(event) { + // Disallow drop from external source unless it's allowed explicitly. + if (!this.vddlDragTypeWorkaround.isDragging && !this.externalSources) { return false; } + + // Check mimetype. Usually we would use a custom drag type instead of Text, but IE doesn't + // support that. + if (!this.hasTextMimetype(event.dataTransfer.types)) { return false; } + + // Now check the allowed-types against the type of the incoming element. For drops from + // external sources we don't know the type, so it will need to be checked via drop. + if (this.allowedTypes && this.vddlDragTypeWorkaround.isDragging) { + var allowed = this.allowedTypes; + if (Array.isArray(allowed) && allowed.indexOf(this.vddlDragTypeWorkaround.dragType) === -1) { + return false; + } + } + + // Check whether droping is disabled completely + if (this.disableIf) { return false; } + + return true; + }, + + /** + * Small helper function that cleans up if we aborted a drop. + */ + stopDragover: function stopDragover() { + this.placeholderNode.parentNode && this.placeholderNode.parentNode.removeChild(this.placeholderNode); + this.$el.className = this.$el.className.replace("vddl-dragover", "").trim(); + return true; + }, + + /** + * Invokes a callback with some interesting parameters and returns the callbacks return value. + */ + invokeCallback: function invokeCallback(expression, event, index, item) { + var fn = this[expression]; + if (fn) { + fn({ + event: event, + index: index, + item: item || undefined, + list: this.list, + external: !this.vddlDragTypeWorkaround.isDragging, + type: this.vddlDragTypeWorkaround.isDragging ? this.vddlDragTypeWorkaround.dragType : undefined + }); + } + return fn ? true : false; + }, + + /** + * Check if the dataTransfer object contains a drag type that we can handle. In old versions + * of IE the types collection will not even be there, so we just assume a drop is possible. + */ + hasTextMimetype: function hasTextMimetype(types) { + if (!types) { return true; } + for (var i = 0; i < types.length; i += 1) { + if (types[i] === "Text" || types[i] === "text/plain") { return true; } + } + + return false; + }, + init: function init() { + this.placeholderNode = this.getPlaceholderElement(); + this.listNode = this.$el; + this.placeholderNode.parentNode && this.placeholderNode.parentNode.removeChild(this.placeholderNode); + }, + }, + ready: function ready() { + this.init(); + }, + mounted: function mounted() { + this.init(); + }, +}; + +var Handle = { template: "
    ", + name: 'vddl-handle', + props: { + handleLeft: Number, + handleTop: Number, + }, + data: function data() { + return {}; + }, + computed: {}, + methods: { + handle: function handle(event) { + event._dndHandle = true; + event._dndHandleLeft = this.handleLeft || 0; + event._dndHandleTop = this.handleTop || 0; + }, + init: function init() { + this.$el.setAttribute('draggable', true); + }, + }, + ready: function ready() { + this.init(); + }, + mounted: function mounted() { + this.init(); + }, +}; + +var Nodrag = { template: "
    ", + name: 'vddl-nodrag', + props: {}, + data: function data() { + return {}; + }, + computed: {}, + methods: { + handleDragstart: function handleDragstart(event) { + if (!event._dndHandle) { + // If a child element already reacted to dragstart and set a dataTransfer object, we will + // allow that. For example, this is the case for user selections inside of input elements. + if (!(event.dataTransfer.types && event.dataTransfer.types.length)) { + event.preventDefault(); + } + event.stopPropagation(); + } + }, + handleDragend: function handleDragend(event) { + if (!event._dndHandle) { + event.stopPropagation(); + } + }, + init: function init() { + this.$el.setAttribute('draggable', true); + }, + }, + ready: function ready() { + this.init(); + }, + mounted: function mounted() { + this.init(); + }, +}; + +var Placeholder = { template: "
    ", + name: 'vddl-placeholder', +}; + +var install = function (Vue) { + /* eslint no-param-reassign: 0 */ + Vue.prototype.vddlDropEffectWorkaround = {}; + Vue.prototype.vddlDragTypeWorkaround = {}; + + Vue.component(Draggable.name, Draggable); + Vue.component(List.name, List); + Vue.component(Handle.name, Handle); + Vue.component(Nodrag.name, Nodrag); + Vue.component(Placeholder.name, Placeholder); +}; + +/* eslint no-undef:0 */ +if (typeof window !== 'undefined' && window.Vue) { + install(window.Vue); +} + +var install$1 = { install: install }; + +return install$1; + +}))); diff --git a/view/adminhtml/web/js/lib/vue-select.js b/view/adminhtml/web/js/lib/vue-select.js new file mode 100755 index 0000000..c166a29 --- /dev/null +++ b/view/adminhtml/web/js/lib/vue-select.js @@ -0,0 +1,2 @@ +!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.VueSelect=e():t.VueSelect=e()}(this,function(){return function(t){function e(r){if(n[r])return n[r].exports;var o=n[r]={exports:{},id:r,loaded:!1};return t[r].call(o.exports,o,o.exports,e),o.loaded=!0,o.exports}var n={};return e.m=t,e.c=n,e.p="/",e(0)}([function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{default:t}}Object.defineProperty(e,"__esModule",{value:!0}),e.mixins=e.VueSelect=void 0;var o=n(83),i=r(o),a=n(42),s=r(a);e.default=i.default,e.VueSelect=i.default,e.mixins=s.default},function(t,e){var n=t.exports="undefined"!=typeof window&&window.Math==Math?window:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();"number"==typeof __g&&(__g=n)},function(t,e,n){t.exports=!n(9)(function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a})},function(t,e){var n={}.hasOwnProperty;t.exports=function(t,e){return n.call(t,e)}},function(t,e,n){var r=n(11),o=n(33),i=n(25),a=Object.defineProperty;e.f=n(2)?Object.defineProperty:function(t,e,n){if(r(t),e=i(e,!0),r(n),o)try{return a(t,e,n)}catch(t){}if("get"in n||"set"in n)throw TypeError("Accessors not supported!");return"value"in n&&(t[e]=n.value),t}},function(t,e){var n=t.exports={version:"2.5.2"};"number"==typeof __e&&(__e=n)},function(t,e,n){var r=n(4),o=n(14);t.exports=n(2)?function(t,e,n){return r.f(t,e,o(1,n))}:function(t,e,n){return t[e]=n,t}},function(t,e,n){var r=n(59),o=n(16);t.exports=function(t){return r(o(t))}},function(t,e,n){var r=n(23)("wks"),o=n(15),i=n(1).Symbol,a="function"==typeof i,s=t.exports=function(t){return r[t]||(r[t]=a&&i[t]||(a?i:o)("Symbol."+t))};s.store=r},function(t,e){t.exports=function(t){try{return!!t()}catch(t){return!0}}},function(t,e){t.exports=function(t){return"object"==typeof t?null!==t:"function"==typeof t}},function(t,e,n){var r=n(10);t.exports=function(t){if(!r(t))throw TypeError(t+" is not an object!");return t}},function(t,e,n){var r=n(1),o=n(5),i=n(56),a=n(6),s="prototype",u=function(t,e,n){var l,c,f,p=t&u.F,d=t&u.G,h=t&u.S,b=t&u.P,v=t&u.B,g=t&u.W,y=d?o:o[e]||(o[e]={}),m=y[s],x=d?r:h?r[e]:(r[e]||{})[s];d&&(n=e);for(l in n)c=!p&&x&&void 0!==x[l],c&&l in y||(f=c?x[l]:n[l],y[l]=d&&"function"!=typeof x[l]?n[l]:v&&c?i(f,r):g&&x[l]==f?function(t){var e=function(e,n,r){if(this instanceof t){switch(arguments.length){case 0:return new t;case 1:return new t(e);case 2:return new t(e,n)}return new t(e,n,r)}return t.apply(this,arguments)};return e[s]=t[s],e}(f):b&&"function"==typeof f?i(Function.call,f):f,b&&((y.virtual||(y.virtual={}))[l]=f,t&u.R&&m&&!m[l]&&a(m,l,f)))};u.F=1,u.G=2,u.S=4,u.P=8,u.B=16,u.W=32,u.U=64,u.R=128,t.exports=u},function(t,e,n){var r=n(38),o=n(17);t.exports=Object.keys||function(t){return r(t,o)}},function(t,e){t.exports=function(t,e){return{enumerable:!(1&t),configurable:!(2&t),writable:!(4&t),value:e}}},function(t,e){var n=0,r=Math.random();t.exports=function(t){return"Symbol(".concat(void 0===t?"":t,")_",(++n+r).toString(36))}},function(t,e){t.exports=function(t){if(void 0==t)throw TypeError("Can't call method on "+t);return t}},function(t,e){t.exports="constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf".split(",")},function(t,e){t.exports={}},function(t,e){t.exports=!0},function(t,e){e.f={}.propertyIsEnumerable},function(t,e,n){var r=n(4).f,o=n(3),i=n(8)("toStringTag");t.exports=function(t,e,n){t&&!o(t=n?t:t.prototype,i)&&r(t,i,{configurable:!0,value:e})}},function(t,e,n){var r=n(23)("keys"),o=n(15);t.exports=function(t){return r[t]||(r[t]=o(t))}},function(t,e,n){var r=n(1),o="__core-js_shared__",i=r[o]||(r[o]={});t.exports=function(t){return i[t]||(i[t]={})}},function(t,e){var n=Math.ceil,r=Math.floor;t.exports=function(t){return isNaN(t=+t)?0:(t>0?r:n)(t)}},function(t,e,n){var r=n(10);t.exports=function(t,e){if(!r(t))return t;var n,o;if(e&&"function"==typeof(n=t.toString)&&!r(o=n.call(t)))return o;if("function"==typeof(n=t.valueOf)&&!r(o=n.call(t)))return o;if(!e&&"function"==typeof(n=t.toString)&&!r(o=n.call(t)))return o;throw TypeError("Can't convert object to primitive value")}},function(t,e,n){var r=n(1),o=n(5),i=n(19),a=n(27),s=n(4).f;t.exports=function(t){var e=o.Symbol||(o.Symbol=i?{}:r.Symbol||{});"_"==t.charAt(0)||t in e||s(e,t,{value:a.f(t)})}},function(t,e,n){e.f=n(8)},function(t,e){"use strict";t.exports={props:{loading:{type:Boolean,default:!1},onSearch:{type:Function,default:function(t,e){}}},data:function(){return{mutableLoading:!1}},watch:{search:function(){this.search.length>0&&(this.onSearch(this.search,this.toggleLoading),this.$emit("search",this.search,this.toggleLoading))},loading:function(t){this.mutableLoading=t}},methods:{toggleLoading:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:null;return null==t?this.mutableLoading=!this.mutableLoading:this.mutableLoading=t}}}},function(t,e){"use strict";t.exports={watch:{typeAheadPointer:function(){this.maybeAdjustScroll()}},methods:{maybeAdjustScroll:function(){var t=this.pixelsToPointerTop(),e=this.pixelsToPointerBottom();return t<=this.viewport().top?this.scrollTo(t):e>=this.viewport().bottom?this.scrollTo(this.viewport().top+this.pointerHeight()):void 0},pixelsToPointerTop:function t(){var t=0;if(this.$refs.dropdownMenu)for(var e=0;e0&&(this.typeAheadPointer--,this.maybeAdjustScroll&&this.maybeAdjustScroll())},typeAheadDown:function(){this.typeAheadPointer";for(e.style.display="none",n(58).appendChild(e),e.src="javascript:",t=e.contentWindow.document,t.open(),t.write(o+"script"+a+"document.F=Object"+o+"/script"+a),t.close(),l=t.F;r--;)delete l[u][i[r]];return l()};t.exports=Object.create||function(t,e){var n;return null!==t?(s[u]=r(t),n=new s,s[u]=null,n[a]=t):n=l(),void 0===e?n:o(n,e)}},function(t,e,n){var r=n(38),o=n(17).concat("length","prototype");e.f=Object.getOwnPropertyNames||function(t){return r(t,o)}},function(t,e){e.f=Object.getOwnPropertySymbols},function(t,e,n){var r=n(3),o=n(7),i=n(55)(!1),a=n(22)("IE_PROTO");t.exports=function(t,e){var n,s=o(t),u=0,l=[];for(n in s)n!=a&&r(s,n)&&l.push(n);for(;e.length>u;)r(s,n=e[u++])&&(~i(l,n)||l.push(n));return l}},function(t,e,n){t.exports=n(6)},function(t,e,n){var r=n(16);t.exports=function(t){return Object(r(t))}},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{default:t}}Object.defineProperty(e,"__esModule",{value:!0});var o=n(44),i=r(o),a=n(47),s=r(a),u=n(48),l=r(u),c=n(29),f=r(c),p=n(30),d=r(p),h=n(28),b=r(h);e.default={mixins:[f.default,d.default,b.default],props:{value:{default:null},options:{type:Array,default:function(){return[]}},disabled:{type:Boolean,default:!1},maxHeight:{type:String,default:"400px"},searchable:{type:Boolean,default:!0},multiple:{type:Boolean,default:!1},placeholder:{type:String,default:""},transition:{type:String,default:"fade"},clearSearchOnSelect:{type:Boolean,default:!0},closeOnSelect:{type:Boolean,default:!0},label:{type:String,default:"label"},getOptionLabel:{type:Function,default:function(t){return"object"===("undefined"==typeof t?"undefined":(0,l.default)(t))&&this.label&&t[this.label]?t[this.label]:t}},onChange:{type:Function,default:function(t){this.$emit("input",t)}},taggable:{type:Boolean,default:!1},tabindex:{type:Number,default:null},pushTags:{type:Boolean,default:!1},filterable:{type:Boolean,default:!0},createOption:{type:Function,default:function(t){return"object"===(0,l.default)(this.mutableOptions[0])&&(t=(0,s.default)({},this.label,t)),this.$emit("option:created",t),t}},resetOnOptionsChange:{type:Boolean,default:!1},noDrop:{type:Boolean,default:!1},inputId:{type:String},dir:{type:String,default:"auto"}},data:function(){return{search:"",open:!1,mutableValue:null,mutableOptions:[]}},watch:{value:function(t){this.mutableValue=t},mutableValue:function(t,e){this.multiple?this.onChange?this.onChange(t):null:this.onChange&&t!==e?this.onChange(t):null},options:function(t){this.mutableOptions=t},mutableOptions:function(){!this.taggable&&this.resetOnOptionsChange&&(this.mutableValue=this.multiple?[]:null)},multiple:function(t){this.mutableValue=t?[]:null}},created:function(){this.mutableValue=this.value,this.mutableOptions=this.options.slice(0),this.mutableLoading=this.loading,this.$on("option:created",this.maybePushTag)},methods:{select:function(t){this.isOptionSelected(t)?this.deselect(t):(this.taggable&&!this.optionExists(t)&&(t=this.createOption(t)),this.multiple&&!this.mutableValue?this.mutableValue=[t]:this.multiple?this.mutableValue.push(t):this.mutableValue=t),this.onAfterSelect(t)},deselect:function(t){var e=this;if(this.multiple){var n=-1;this.mutableValue.forEach(function(r){(r===t||"object"===("undefined"==typeof r?"undefined":(0,l.default)(r))&&r[e.label]===t[e.label])&&(n=r)});var r=this.mutableValue.indexOf(n);this.mutableValue.splice(r,1)}else this.mutableValue=null},clearSelection:function(){this.mutableValue=this.multiple?[]:null},onAfterSelect:function(t){this.closeOnSelect&&(this.open=!this.open,this.$refs.search.blur()),this.clearSearchOnSelect&&(this.search="")},toggleDropdown:function(t){t.target!==this.$refs.openIndicator&&t.target!==this.$refs.search&&t.target!==this.$refs.toggle&&t.target!==this.$el||(this.open?this.$refs.search.blur():this.disabled||(this.open=!0,this.$refs.search.focus()))},isOptionSelected:function(t){var e=this;if(this.multiple&&this.mutableValue){var n=!1;return this.mutableValue.forEach(function(r){"object"===("undefined"==typeof r?"undefined":(0,l.default)(r))&&r[e.label]===t[e.label]?n=!0:"object"===("undefined"==typeof r?"undefined":(0,l.default)(r))&&r[e.label]===t?n=!0:r===t&&(n=!0)}),n}return this.mutableValue===t},onEscape:function(){this.search.length?this.search="":this.$refs.search.blur()},onSearchBlur:function(){this.clearSearchOnBlur&&(this.search=""),this.open=!1,this.$emit("search:blur")},onSearchFocus:function(){this.open=!0,this.$emit("search:focus")},maybeDeleteValue:function(){if(!this.$refs.search.value.length&&this.mutableValue)return this.multiple?this.mutableValue.pop():this.mutableValue=null},optionExists:function(t){var e=this,n=!1;return this.mutableOptions.forEach(function(r){"object"===("undefined"==typeof r?"undefined":(0,l.default)(r))&&r[e.label]===t?n=!0:r===t&&(n=!0)}),n},maybePushTag:function(t){this.pushTags&&this.mutableOptions.push(t)}},computed:{dropdownClasses:function(){return{open:this.dropdownOpen,single:!this.multiple,searching:this.searching,searchable:this.searchable,unsearchable:!this.searchable,loading:this.mutableLoading,rtl:"rtl"===this.dir,disabled:this.disabled}},clearSearchOnBlur:function(){return this.clearSearchOnSelect&&!this.multiple},searching:function(){return!!this.search},dropdownOpen:function(){return!this.noDrop&&(this.open&&!this.mutableLoading)},searchPlaceholder:function(){if(this.isValueEmpty&&this.placeholder)return this.placeholder},filteredOptions:function(){var t=this;if(!this.filterable&&!this.taggable)return this.mutableOptions.slice();var e=this.mutableOptions.filter(function(e){return"object"===("undefined"==typeof e?"undefined":(0,l.default)(e))&&e.hasOwnProperty(t.label)?e[t.label].toLowerCase().indexOf(t.search.toLowerCase())>-1:"object"!==("undefined"==typeof e?"undefined":(0,l.default)(e))||e.hasOwnProperty(t.label)?e.toLowerCase().indexOf(t.search.toLowerCase())>-1:console.warn('[vue-select warn]: Label key "option.'+t.label+'" does not exist in options object.\nhttp://sagalbot.github.io/vue-select/#ex-labels')});return this.taggable&&this.search.length&&!this.optionExists(this.search)&&e.unshift(this.search),e},isValueEmpty:function(){return!this.mutableValue||("object"===(0,l.default)(this.mutableValue)?!(0,i.default)(this.mutableValue).length:!this.mutableValue.length)},valueAsArray:function(){return this.multiple?this.mutableValue:this.mutableValue?[this.mutableValue]:[]},showClearButton:function(){return!this.multiple&&!this.open&&null!=this.mutableValue}}}},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{default:t}}Object.defineProperty(e,"__esModule",{value:!0});var o=n(28),i=r(o),a=n(30),s=r(a),u=n(29),l=r(u);e.default={ajax:i.default,pointer:s.default,pointerScroll:l.default}},function(t,e,n){t.exports={default:n(49),__esModule:!0}},function(t,e,n){t.exports={default:n(50),__esModule:!0}},function(t,e,n){t.exports={default:n(51),__esModule:!0}},function(t,e,n){t.exports={default:n(52),__esModule:!0}},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{default:t}}e.__esModule=!0;var o=n(43),i=r(o);e.default=function(t,e,n){return e in t?(0,i.default)(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{default:t}}e.__esModule=!0;var o=n(46),i=r(o),a=n(45),s=r(a),u="function"==typeof s.default&&"symbol"==typeof i.default?function(t){return typeof t}:function(t){return t&&"function"==typeof s.default&&t.constructor===s.default&&t!==s.default.prototype?"symbol":typeof t};e.default="function"==typeof s.default&&"symbol"===u(i.default)?function(t){return"undefined"==typeof t?"undefined":u(t)}:function(t){return t&&"function"==typeof s.default&&t.constructor===s.default&&t!==s.default.prototype?"symbol":"undefined"==typeof t?"undefined":u(t)}},function(t,e,n){n(73);var r=n(5).Object;t.exports=function(t,e,n){return r.defineProperty(t,e,n)}},function(t,e,n){n(74),t.exports=n(5).Object.keys},function(t,e,n){n(77),n(75),n(78),n(79),t.exports=n(5).Symbol},function(t,e,n){n(76),n(80),t.exports=n(27).f("iterator")},function(t,e){t.exports=function(t){if("function"!=typeof t)throw TypeError(t+" is not a function!");return t}},function(t,e){t.exports=function(){}},function(t,e,n){var r=n(7),o=n(71),i=n(70);t.exports=function(t){return function(e,n,a){var s,u=r(e),l=o(u.length),c=i(a,l);if(t&&n!=n){for(;l>c;)if(s=u[c++],s!=s)return!0}else for(;l>c;c++)if((t||c in u)&&u[c]===n)return t||c||0;return!t&&-1}}},function(t,e,n){var r=n(53);t.exports=function(t,e,n){if(r(t),void 0===e)return t;switch(n){case 1:return function(n){return t.call(e,n)};case 2:return function(n,r){return t.call(e,n,r)};case 3:return function(n,r,o){return t.call(e,n,r,o)}}return function(){return t.apply(e,arguments)}}},function(t,e,n){var r=n(13),o=n(37),i=n(20);t.exports=function(t){var e=r(t),n=o.f;if(n)for(var a,s=n(t),u=i.f,l=0;s.length>l;)u.call(t,a=s[l++])&&e.push(a);return e}},function(t,e,n){var r=n(1).document;t.exports=r&&r.documentElement},function(t,e,n){var r=n(31);t.exports=Object("z").propertyIsEnumerable(0)?Object:function(t){return"String"==r(t)?t.split(""):Object(t)}},function(t,e,n){var r=n(31);t.exports=Array.isArray||function(t){return"Array"==r(t)}},function(t,e,n){"use strict";var r=n(35),o=n(14),i=n(21),a={};n(6)(a,n(8)("iterator"),function(){return this}),t.exports=function(t,e,n){t.prototype=r(a,{next:o(1,n)}),i(t,e+" Iterator")}},function(t,e){t.exports=function(t,e){return{value:e,done:!!t}}},function(t,e,n){var r=n(15)("meta"),o=n(10),i=n(3),a=n(4).f,s=0,u=Object.isExtensible||function(){return!0},l=!n(9)(function(){return u(Object.preventExtensions({}))}),c=function(t){a(t,r,{value:{i:"O"+ ++s,w:{}}})},f=function(t,e){if(!o(t))return"symbol"==typeof t?t:("string"==typeof t?"S":"P")+t;if(!i(t,r)){if(!u(t))return"F";if(!e)return"E";c(t)}return t[r].i},p=function(t,e){if(!i(t,r)){if(!u(t))return!0;if(!e)return!1;c(t)}return t[r].w},d=function(t){return l&&h.NEED&&u(t)&&!i(t,r)&&c(t),t},h=t.exports={KEY:r,NEED:!1,fastKey:f,getWeak:p,onFreeze:d}},function(t,e,n){var r=n(4),o=n(11),i=n(13);t.exports=n(2)?Object.defineProperties:function(t,e){o(t);for(var n,a=i(e),s=a.length,u=0;s>u;)r.f(t,n=a[u++],e[n]);return t}},function(t,e,n){var r=n(20),o=n(14),i=n(7),a=n(25),s=n(3),u=n(33),l=Object.getOwnPropertyDescriptor;e.f=n(2)?l:function(t,e){if(t=i(t),e=a(e,!0),u)try{return l(t,e)}catch(t){}if(s(t,e))return o(!r.f.call(t,e),t[e])}},function(t,e,n){var r=n(7),o=n(36).f,i={}.toString,a="object"==typeof window&&window&&Object.getOwnPropertyNames?Object.getOwnPropertyNames(window):[],s=function(t){try{return o(t)}catch(t){return a.slice()}};t.exports.f=function(t){return a&&"[object Window]"==i.call(t)?s(t):o(r(t))}},function(t,e,n){var r=n(3),o=n(40),i=n(22)("IE_PROTO"),a=Object.prototype;t.exports=Object.getPrototypeOf||function(t){return t=o(t),r(t,i)?t[i]:"function"==typeof t.constructor&&t instanceof t.constructor?t.constructor.prototype:t instanceof Object?a:null}},function(t,e,n){var r=n(12),o=n(5),i=n(9);t.exports=function(t,e){var n=(o.Object||{})[t]||Object[t],a={};a[t]=e(n),r(r.S+r.F*i(function(){n(1)}),"Object",a)}},function(t,e,n){var r=n(24),o=n(16);t.exports=function(t){return function(e,n){var i,a,s=String(o(e)),u=r(n),l=s.length;return u<0||u>=l?t?"":void 0:(i=s.charCodeAt(u),i<55296||i>56319||u+1===l||(a=s.charCodeAt(u+1))<56320||a>57343?t?s.charAt(u):i:t?s.slice(u,u+2):(i-55296<<10)+(a-56320)+65536)}}},function(t,e,n){var r=n(24),o=Math.max,i=Math.min;t.exports=function(t,e){return t=r(t),t<0?o(t+e,0):i(t,e)}},function(t,e,n){var r=n(24),o=Math.min;t.exports=function(t){return t>0?o(r(t),9007199254740991):0}},function(t,e,n){"use strict";var r=n(54),o=n(62),i=n(18),a=n(7);t.exports=n(34)(Array,"Array",function(t,e){this._t=a(t),this._i=0,this._k=e},function(){var t=this._t,e=this._k,n=this._i++;return!t||n>=t.length?(this._t=void 0,o(1)):"keys"==e?o(0,n):"values"==e?o(0,t[n]):o(0,[n,t[n]])},"values"),i.Arguments=i.Array,r("keys"),r("values"),r("entries")},function(t,e,n){var r=n(12);r(r.S+r.F*!n(2),"Object",{defineProperty:n(4).f})},function(t,e,n){var r=n(40),o=n(13);n(68)("keys",function(){return function(t){return o(r(t))}})},function(t,e){},function(t,e,n){"use strict";var r=n(69)(!0);n(34)(String,"String",function(t){this._t=String(t),this._i=0},function(){var t,e=this._t,n=this._i;return n>=e.length?{value:void 0,done:!0}:(t=r(e,n),this._i+=t.length,{value:t,done:!1})})},function(t,e,n){"use strict";var r=n(1),o=n(3),i=n(2),a=n(12),s=n(39),u=n(63).KEY,l=n(9),c=n(23),f=n(21),p=n(15),d=n(8),h=n(27),b=n(26),v=n(57),g=n(60),y=n(11),m=n(10),x=n(7),w=n(25),S=n(14),O=n(35),_=n(66),j=n(65),k=n(4),P=n(13),C=j.f,A=k.f,M=_.f,L=r.Symbol,T=r.JSON,E=T&&T.stringify,V="prototype",B=d("_hidden"),F=d("toPrimitive"),$={}.propertyIsEnumerable,N=c("symbol-registry"),D=c("symbols"),I=c("op-symbols"),R=Object[V],z="function"==typeof L,H=r.QObject,G=!H||!H[V]||!H[V].findChild,U=i&&l(function(){return 7!=O(A({},"a",{get:function(){return A(this,"a",{value:7}).a}})).a})?function(t,e,n){var r=C(R,e);r&&delete R[e],A(t,e,n),r&&t!==R&&A(R,e,r)}:A,W=function(t){var e=D[t]=O(L[V]);return e._k=t,e},J=z&&"symbol"==typeof L.iterator?function(t){return"symbol"==typeof t}:function(t){return t instanceof L},K=function(t,e,n){return t===R&&K(I,e,n),y(t),e=w(e,!0),y(n),o(D,e)?(n.enumerable?(o(t,B)&&t[B][e]&&(t[B][e]=!1),n=O(n,{enumerable:S(0,!1)})):(o(t,B)||A(t,B,S(1,{})),t[B][e]=!0),U(t,e,n)):A(t,e,n)},Y=function(t,e){y(t);for(var n,r=v(e=x(e)),o=0,i=r.length;i>o;)K(t,n=r[o++],e[n]);return t},q=function(t,e){return void 0===e?O(t):Y(O(t),e)},Q=function(t){var e=$.call(this,t=w(t,!0));return!(this===R&&o(D,t)&&!o(I,t))&&(!(e||!o(this,t)||!o(D,t)||o(this,B)&&this[B][t])||e)},Z=function(t,e){if(t=x(t),e=w(e,!0),t!==R||!o(D,e)||o(I,e)){var n=C(t,e);return!n||!o(D,e)||o(t,B)&&t[B][e]||(n.enumerable=!0),n}},X=function(t){for(var e,n=M(x(t)),r=[],i=0;n.length>i;)o(D,e=n[i++])||e==B||e==u||r.push(e);return r},tt=function(t){for(var e,n=t===R,r=M(n?I:x(t)),i=[],a=0;r.length>a;)!o(D,e=r[a++])||n&&!o(R,e)||i.push(D[e]);return i};z||(L=function(){if(this instanceof L)throw TypeError("Symbol is not a constructor!");var t=p(arguments.length>0?arguments[0]:void 0),e=function(n){this===R&&e.call(I,n),o(this,B)&&o(this[B],t)&&(this[B][t]=!1),U(this,t,S(1,n))};return i&&G&&U(R,t,{configurable:!0,set:e}),W(t)},s(L[V],"toString",function(){return this._k}),j.f=Z,k.f=K,n(36).f=_.f=X,n(20).f=Q,n(37).f=tt,i&&!n(19)&&s(R,"propertyIsEnumerable",Q,!0),h.f=function(t){return W(d(t))}),a(a.G+a.W+a.F*!z,{Symbol:L});for(var et="hasInstance,isConcatSpreadable,iterator,match,replace,search,species,split,toPrimitive,toStringTag,unscopables".split(","),nt=0;et.length>nt;)d(et[nt++]);for(var rt=P(d.store),ot=0;rt.length>ot;)b(rt[ot++]);a(a.S+a.F*!z,"Symbol",{for:function(t){return o(N,t+="")?N[t]:N[t]=L(t)},keyFor:function(t){if(!J(t))throw TypeError(t+" is not a symbol!");for(var e in N)if(N[e]===t)return e},useSetter:function(){G=!0},useSimple:function(){G=!1}}),a(a.S+a.F*!z,"Object",{create:q,defineProperty:K,defineProperties:Y,getOwnPropertyDescriptor:Z,getOwnPropertyNames:X,getOwnPropertySymbols:tt}),T&&a(a.S+a.F*(!z||l(function(){var t=L();return"[null]"!=E([t])||"{}"!=E({a:t})||"{}"!=E(Object(t))})),"JSON",{stringify:function(t){for(var e,n,r=[t],o=1;arguments.length>o;)r.push(arguments[o++]);if(n=e=r[1],(m(e)||void 0!==t)&&!J(t))return g(e)||(e=function(t,e){if(n&&(e=n.call(this,t,e)),!J(e))return e}),r[1]=e,E.apply(T,r)}}),L[V][F]||n(6)(L[V],F,L[V].valueOf),f(L,"Symbol"),f(Math,"Math",!0),f(r.JSON,"JSON",!0)},function(t,e,n){n(26)("asyncIterator")},function(t,e,n){n(26)("observable")},function(t,e,n){n(72);for(var r=n(1),o=n(6),i=n(18),a=n(8)("toStringTag"),s="CSSRuleList,CSSStyleDeclaration,CSSValueList,ClientRectList,DOMRectList,DOMStringList,DOMTokenList,DataTransferItemList,FileList,HTMLAllCollection,HTMLCollection,HTMLFormElement,HTMLSelectElement,MediaList,MimeTypeArray,NamedNodeMap,NodeList,PaintRequestList,Plugin,PluginArray,SVGLengthList,SVGNumberList,SVGPathSegList,SVGPointList,SVGStringList,SVGTransformList,SourceBufferList,StyleSheetList,TextTrackCueList,TextTrackList,TouchList".split(","),u=0;ua{display:block;padding:3px 20px;clear:both;color:#333;white-space:nowrap}.v-select li:hover{cursor:pointer}.v-select .dropdown-menu .active>a{color:#333;background:rgba(50,50,50,.1)}.v-select .dropdown-menu>.highlight>a{background:#5897fb;color:#fff}.v-select .highlight:not(:last-child){margin-bottom:0}.v-select .spinner{opacity:0;position:absolute;top:5px;right:10px;font-size:5px;text-indent:-9999em;overflow:hidden;border-top:.9em solid hsla(0,0%,39%,.1);border-right:.9em solid hsla(0,0%,39%,.1);border-bottom:.9em solid hsla(0,0%,39%,.1);border-left:.9em solid rgba(60,60,60,.45);transform:translateZ(0);animation:vSelectSpinner 1.1s infinite linear;transition:opacity .1s}.v-select .spinner,.v-select .spinner:after{border-radius:50%;width:5em;height:5em}.v-select.disabled .dropdown-toggle,.v-select.disabled .dropdown-toggle .clear,.v-select.disabled .dropdown-toggle input,.v-select.disabled .open-indicator,.v-select.disabled .selected-tag .close{cursor:not-allowed;background-color:#f8f8f8}.v-select.loading .spinner{opacity:1}@-webkit-keyframes vSelectSpinner{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}@keyframes vSelectSpinner{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}.fade-enter-active,.fade-leave-active{transition:opacity .15s cubic-bezier(1,.5,.8,1)}.fade-enter,.fade-leave-to{opacity:0}',""])},function(t,e){t.exports=function(){var t=[];return t.toString=function(){for(var t=[],e=0;e=0&&g.splice(e,1)}function s(t){var e=document.createElement("style");return e.type="text/css",i(t,e),e}function u(t,e){var n,r,o;if(e.singleton){var i=v++;n=b||(b=s(e)),r=l.bind(null,n,i,!1),o=l.bind(null,n,i,!0)}else n=s(e),r=c.bind(null,n),o=function(){a(n)};return r(t),function(e){if(e){if(e.css===t.css&&e.media===t.media&&e.sourceMap===t.sourceMap)return;r(t=e)}else o()}}function l(t,e,n,r){var o=n?"":r.css;if(t.styleSheet)t.styleSheet.cssText=y(e,o);else{var i=document.createTextNode(o),a=t.childNodes;a[e]&&t.removeChild(a[e]),a.length?t.insertBefore(i,a[e]):t.appendChild(i)}}function c(t,e){var n=e.css,r=e.media,o=e.sourceMap;if(r&&t.setAttribute("media",r),o&&(n+="\n/*# sourceURL="+o.sources[0]+" */",n+="\n/*# sourceMappingURL=data:application/json;base64,"+btoa(unescape(encodeURIComponent(JSON.stringify(o))))+" */"),t.styleSheet)t.styleSheet.cssText=n;else{for(;t.firstChild;)t.removeChild(t.firstChild);t.appendChild(document.createTextNode(n))}}var f={},p=function(t){var e;return function(){return"undefined"==typeof e&&(e=t.apply(this,arguments)),e}},d=p(function(){return/msie [6-9]\b/.test(window.navigator.userAgent.toLowerCase())}),h=p(function(){return document.head||document.getElementsByTagName("head")[0]}),b=null,v=0,g=[];t.exports=function(t,e){e=e||{},"undefined"==typeof e.singleton&&(e.singleton=d()),"undefined"==typeof e.insertAt&&(e.insertAt="bottom");var n=o(t);return r(n,e),function(t){for(var i=[],a=0;a= 0 && Math.floor(n) === n && isFinite(val) +} + +/** + * Convert a value to a string that is actually rendered. + */ +function toString (val) { + return val == null + ? '' + : typeof val === 'object' + ? JSON.stringify(val, null, 2) + : String(val) +} + +/** + * Convert a input value to a number for persistence. + * If the conversion fails, return original string. + */ +function toNumber (val) { + var n = parseFloat(val); + return isNaN(n) ? val : n +} + +/** + * Make a map and return a function for checking if a key + * is in that map. + */ +function makeMap ( + str, + expectsLowerCase +) { + var map = Object.create(null); + var list = str.split(','); + for (var i = 0; i < list.length; i++) { + map[list[i]] = true; + } + return expectsLowerCase + ? function (val) { return map[val.toLowerCase()]; } + : function (val) { return map[val]; } +} + +/** + * Check if a tag is a built-in tag. + */ +var isBuiltInTag = makeMap('slot,component', true); + +/** + * Check if a attribute is a reserved attribute. + */ +var isReservedAttribute = makeMap('key,ref,slot,slot-scope,is'); + +/** + * Remove an item from an array + */ +function remove (arr, item) { + if (arr.length) { + var index = arr.indexOf(item); + if (index > -1) { + return arr.splice(index, 1) + } + } +} + +/** + * Check whether the object has the property. + */ +var hasOwnProperty = Object.prototype.hasOwnProperty; +function hasOwn (obj, key) { + return hasOwnProperty.call(obj, key) +} + +/** + * Create a cached version of a pure function. + */ +function cached (fn) { + var cache = Object.create(null); + return (function cachedFn (str) { + var hit = cache[str]; + return hit || (cache[str] = fn(str)) + }) +} + +/** + * Camelize a hyphen-delimited string. + */ +var camelizeRE = /-(\w)/g; +var camelize = cached(function (str) { + return str.replace(camelizeRE, function (_, c) { return c ? c.toUpperCase() : ''; }) +}); + +/** + * Capitalize a string. + */ +var capitalize = cached(function (str) { + return str.charAt(0).toUpperCase() + str.slice(1) +}); + +/** + * Hyphenate a camelCase string. + */ +var hyphenateRE = /\B([A-Z])/g; +var hyphenate = cached(function (str) { + return str.replace(hyphenateRE, '-$1').toLowerCase() +}); + +/** + * Simple bind, faster than native + */ +function bind (fn, ctx) { + function boundFn (a) { + var l = arguments.length; + return l + ? l > 1 + ? fn.apply(ctx, arguments) + : fn.call(ctx, a) + : fn.call(ctx) + } + // record original fn length + boundFn._length = fn.length; + return boundFn +} + +/** + * Convert an Array-like object to a real Array. + */ +function toArray (list, start) { + start = start || 0; + var i = list.length - start; + var ret = new Array(i); + while (i--) { + ret[i] = list[i + start]; + } + return ret +} + +/** + * Mix properties into target object. + */ +function extend (to, _from) { + for (var key in _from) { + to[key] = _from[key]; + } + return to +} + +/** + * Merge an Array of Objects into a single Object. + */ +function toObject (arr) { + var res = {}; + for (var i = 0; i < arr.length; i++) { + if (arr[i]) { + extend(res, arr[i]); + } + } + return res +} + +/** + * Perform no operation. + * Stubbing args to make Flow happy without leaving useless transpiled code + * with ...rest (https://flow.org/blog/2017/05/07/Strict-Function-Call-Arity/) + */ +function noop (a, b, c) {} + +/** + * Always return false. + */ +var no = function (a, b, c) { return false; }; + +/** + * Return same value + */ +var identity = function (_) { return _; }; + +/** + * Generate a static keys string from compiler modules. + */ +function genStaticKeys (modules) { + return modules.reduce(function (keys, m) { + return keys.concat(m.staticKeys || []) + }, []).join(',') +} + +/** + * Check if two values are loosely equal - that is, + * if they are plain objects, do they have the same shape? + */ +function looseEqual (a, b) { + if (a === b) { return true } + var isObjectA = isObject(a); + var isObjectB = isObject(b); + if (isObjectA && isObjectB) { + try { + var isArrayA = Array.isArray(a); + var isArrayB = Array.isArray(b); + if (isArrayA && isArrayB) { + return a.length === b.length && a.every(function (e, i) { + return looseEqual(e, b[i]) + }) + } else if (!isArrayA && !isArrayB) { + var keysA = Object.keys(a); + var keysB = Object.keys(b); + return keysA.length === keysB.length && keysA.every(function (key) { + return looseEqual(a[key], b[key]) + }) + } else { + /* istanbul ignore next */ + return false + } + } catch (e) { + /* istanbul ignore next */ + return false + } + } else if (!isObjectA && !isObjectB) { + return String(a) === String(b) + } else { + return false + } +} + +function looseIndexOf (arr, val) { + for (var i = 0; i < arr.length; i++) { + if (looseEqual(arr[i], val)) { return i } + } + return -1 +} + +/** + * Ensure a function is called only once. + */ +function once (fn) { + var called = false; + return function () { + if (!called) { + called = true; + fn.apply(this, arguments); + } + } +} + +var SSR_ATTR = 'data-server-rendered'; + +var ASSET_TYPES = [ + 'component', + 'directive', + 'filter' +]; + +var LIFECYCLE_HOOKS = [ + 'beforeCreate', + 'created', + 'beforeMount', + 'mounted', + 'beforeUpdate', + 'updated', + 'beforeDestroy', + 'destroyed', + 'activated', + 'deactivated', + 'errorCaptured' +]; + +/* */ + +var config = ({ + /** + * Option merge strategies (used in core/util/options) + */ + // $flow-disable-line + optionMergeStrategies: Object.create(null), + + /** + * Whether to suppress warnings. + */ + silent: false, + + /** + * Show production mode tip message on boot? + */ + productionTip: "development" !== 'production', + + /** + * Whether to enable devtools + */ + devtools: "development" !== 'production', + + /** + * Whether to record perf + */ + performance: false, + + /** + * Error handler for watcher errors + */ + errorHandler: null, + + /** + * Warn handler for watcher warns + */ + warnHandler: null, + + /** + * Ignore certain custom elements + */ + ignoredElements: [], + + /** + * Custom user key aliases for v-on + */ + // $flow-disable-line + keyCodes: Object.create(null), + + /** + * Check if a tag is reserved so that it cannot be registered as a + * component. This is platform-dependent and may be overwritten. + */ + isReservedTag: no, + + /** + * Check if an attribute is reserved so that it cannot be used as a component + * prop. This is platform-dependent and may be overwritten. + */ + isReservedAttr: no, + + /** + * Check if a tag is an unknown element. + * Platform-dependent. + */ + isUnknownElement: no, + + /** + * Get the namespace of an element + */ + getTagNamespace: noop, + + /** + * Parse the real tag name for the specific platform. + */ + parsePlatformTagName: identity, + + /** + * Check if an attribute must be bound using property, e.g. value + * Platform-dependent. + */ + mustUseProp: no, + + /** + * Exposed for legacy reasons + */ + _lifecycleHooks: LIFECYCLE_HOOKS +}); + +/* */ + +/** + * Check if a string starts with $ or _ + */ +function isReserved (str) { + var c = (str + '').charCodeAt(0); + return c === 0x24 || c === 0x5F +} + +/** + * Define a property. + */ +function def (obj, key, val, enumerable) { + Object.defineProperty(obj, key, { + value: val, + enumerable: !!enumerable, + writable: true, + configurable: true + }); +} + +/** + * Parse simple path. + */ +var bailRE = /[^\w.$]/; +function parsePath (path) { + if (bailRE.test(path)) { + return + } + var segments = path.split('.'); + return function (obj) { + for (var i = 0; i < segments.length; i++) { + if (!obj) { return } + obj = obj[segments[i]]; + } + return obj + } +} + +/* */ + + +// can we use __proto__? +var hasProto = '__proto__' in {}; + +// Browser environment sniffing +var inBrowser = typeof window !== 'undefined'; +var inWeex = typeof WXEnvironment !== 'undefined' && !!WXEnvironment.platform; +var weexPlatform = inWeex && WXEnvironment.platform.toLowerCase(); +var UA = inBrowser && window.navigator.userAgent.toLowerCase(); +var isIE = UA && /msie|trident/.test(UA); +var isIE9 = UA && UA.indexOf('msie 9.0') > 0; +var isEdge = UA && UA.indexOf('edge/') > 0; +var isAndroid = (UA && UA.indexOf('android') > 0) || (weexPlatform === 'android'); +var isIOS = (UA && /iphone|ipad|ipod|ios/.test(UA)) || (weexPlatform === 'ios'); +var isChrome = UA && /chrome\/\d+/.test(UA) && !isEdge; + +// Firefox has a "watch" function on Object.prototype... +var nativeWatch = ({}).watch; + +var supportsPassive = false; +if (inBrowser) { + try { + var opts = {}; + Object.defineProperty(opts, 'passive', ({ + get: function get () { + /* istanbul ignore next */ + supportsPassive = true; + } + })); // https://github.com/facebook/flow/issues/285 + window.addEventListener('test-passive', null, opts); + } catch (e) {} +} + +// this needs to be lazy-evaled because vue may be required before +// vue-server-renderer can set VUE_ENV +var _isServer; +var isServerRendering = function () { + if (_isServer === undefined) { + /* istanbul ignore if */ + if (!inBrowser && typeof global !== 'undefined') { + // detect presence of vue-server-renderer and avoid + // Webpack shimming the process + _isServer = global['process'].env.VUE_ENV === 'server'; + } else { + _isServer = false; + } + } + return _isServer +}; + +// detect devtools +var devtools = inBrowser && window.__VUE_DEVTOOLS_GLOBAL_HOOK__; + +/* istanbul ignore next */ +function isNative (Ctor) { + return typeof Ctor === 'function' && /native code/.test(Ctor.toString()) +} + +var hasSymbol = + typeof Symbol !== 'undefined' && isNative(Symbol) && + typeof Reflect !== 'undefined' && isNative(Reflect.ownKeys); + +var _Set; +/* istanbul ignore if */ // $flow-disable-line +if (typeof Set !== 'undefined' && isNative(Set)) { + // use native Set when available. + _Set = Set; +} else { + // a non-standard Set polyfill that only works with primitive keys. + _Set = (function () { + function Set () { + this.set = Object.create(null); + } + Set.prototype.has = function has (key) { + return this.set[key] === true + }; + Set.prototype.add = function add (key) { + this.set[key] = true; + }; + Set.prototype.clear = function clear () { + this.set = Object.create(null); + }; + + return Set; + }()); +} + +/* */ + +var warn = noop; +var tip = noop; +var generateComponentTrace = (noop); // work around flow check +var formatComponentName = (noop); + +{ + var hasConsole = typeof console !== 'undefined'; + var classifyRE = /(?:^|[-_])(\w)/g; + var classify = function (str) { return str + .replace(classifyRE, function (c) { return c.toUpperCase(); }) + .replace(/[-_]/g, ''); }; + + warn = function (msg, vm) { + var trace = vm ? generateComponentTrace(vm) : ''; + + if (config.warnHandler) { + config.warnHandler.call(null, msg, vm, trace); + } else if (hasConsole && (!config.silent)) { + console.error(("[Vue warn]: " + msg + trace)); + } + }; + + tip = function (msg, vm) { + if (hasConsole && (!config.silent)) { + console.warn("[Vue tip]: " + msg + ( + vm ? generateComponentTrace(vm) : '' + )); + } + }; + + formatComponentName = function (vm, includeFile) { + if (vm.$root === vm) { + return '' + } + var options = typeof vm === 'function' && vm.cid != null + ? vm.options + : vm._isVue + ? vm.$options || vm.constructor.options + : vm || {}; + var name = options.name || options._componentTag; + var file = options.__file; + if (!name && file) { + var match = file.match(/([^/\\]+)\.vue$/); + name = match && match[1]; + } + + return ( + (name ? ("<" + (classify(name)) + ">") : "") + + (file && includeFile !== false ? (" at " + file) : '') + ) + }; + + var repeat = function (str, n) { + var res = ''; + while (n) { + if (n % 2 === 1) { res += str; } + if (n > 1) { str += str; } + n >>= 1; + } + return res + }; + + generateComponentTrace = function (vm) { + if (vm._isVue && vm.$parent) { + var tree = []; + var currentRecursiveSequence = 0; + while (vm) { + if (tree.length > 0) { + var last = tree[tree.length - 1]; + if (last.constructor === vm.constructor) { + currentRecursiveSequence++; + vm = vm.$parent; + continue + } else if (currentRecursiveSequence > 0) { + tree[tree.length - 1] = [last, currentRecursiveSequence]; + currentRecursiveSequence = 0; + } + } + tree.push(vm); + vm = vm.$parent; + } + return '\n\nfound in\n\n' + tree + .map(function (vm, i) { return ("" + (i === 0 ? '---> ' : repeat(' ', 5 + i * 2)) + (Array.isArray(vm) + ? ((formatComponentName(vm[0])) + "... (" + (vm[1]) + " recursive calls)") + : formatComponentName(vm))); }) + .join('\n') + } else { + return ("\n\n(found in " + (formatComponentName(vm)) + ")") + } + }; +} + +/* */ + + +var uid = 0; + +/** + * A dep is an observable that can have multiple + * directives subscribing to it. + */ +var Dep = function Dep () { + this.id = uid++; + this.subs = []; +}; + +Dep.prototype.addSub = function addSub (sub) { + this.subs.push(sub); +}; + +Dep.prototype.removeSub = function removeSub (sub) { + remove(this.subs, sub); +}; + +Dep.prototype.depend = function depend () { + if (Dep.target) { + Dep.target.addDep(this); + } +}; + +Dep.prototype.notify = function notify () { + // stabilize the subscriber list first + var subs = this.subs.slice(); + for (var i = 0, l = subs.length; i < l; i++) { + subs[i].update(); + } +}; + +// the current target watcher being evaluated. +// this is globally unique because there could be only one +// watcher being evaluated at any time. +Dep.target = null; +var targetStack = []; + +function pushTarget (_target) { + if (Dep.target) { targetStack.push(Dep.target); } + Dep.target = _target; +} + +function popTarget () { + Dep.target = targetStack.pop(); +} + +/* */ + +var VNode = function VNode ( + tag, + data, + children, + text, + elm, + context, + componentOptions, + asyncFactory +) { + this.tag = tag; + this.data = data; + this.children = children; + this.text = text; + this.elm = elm; + this.ns = undefined; + this.context = context; + this.fnContext = undefined; + this.fnOptions = undefined; + this.fnScopeId = undefined; + this.key = data && data.key; + this.componentOptions = componentOptions; + this.componentInstance = undefined; + this.parent = undefined; + this.raw = false; + this.isStatic = false; + this.isRootInsert = true; + this.isComment = false; + this.isCloned = false; + this.isOnce = false; + this.asyncFactory = asyncFactory; + this.asyncMeta = undefined; + this.isAsyncPlaceholder = false; +}; + +var prototypeAccessors = { child: { configurable: true } }; + +// DEPRECATED: alias for componentInstance for backwards compat. +/* istanbul ignore next */ +prototypeAccessors.child.get = function () { + return this.componentInstance +}; + +Object.defineProperties( VNode.prototype, prototypeAccessors ); + +var createEmptyVNode = function (text) { + if ( text === void 0 ) text = ''; + + var node = new VNode(); + node.text = text; + node.isComment = true; + return node +}; + +function createTextVNode (val) { + return new VNode(undefined, undefined, undefined, String(val)) +} + +// optimized shallow clone +// used for static nodes and slot nodes because they may be reused across +// multiple renders, cloning them avoids errors when DOM manipulations rely +// on their elm reference. +function cloneVNode (vnode, deep) { + var componentOptions = vnode.componentOptions; + var cloned = new VNode( + vnode.tag, + vnode.data, + vnode.children, + vnode.text, + vnode.elm, + vnode.context, + componentOptions, + vnode.asyncFactory + ); + cloned.ns = vnode.ns; + cloned.isStatic = vnode.isStatic; + cloned.key = vnode.key; + cloned.isComment = vnode.isComment; + cloned.fnContext = vnode.fnContext; + cloned.fnOptions = vnode.fnOptions; + cloned.fnScopeId = vnode.fnScopeId; + cloned.isCloned = true; + if (deep) { + if (vnode.children) { + cloned.children = cloneVNodes(vnode.children, true); + } + if (componentOptions && componentOptions.children) { + componentOptions.children = cloneVNodes(componentOptions.children, true); + } + } + return cloned +} + +function cloneVNodes (vnodes, deep) { + var len = vnodes.length; + var res = new Array(len); + for (var i = 0; i < len; i++) { + res[i] = cloneVNode(vnodes[i], deep); + } + return res +} + +/* + * not type checking this file because flow doesn't play well with + * dynamically accessing methods on Array prototype + */ + +var arrayProto = Array.prototype; +var arrayMethods = Object.create(arrayProto);[ + 'push', + 'pop', + 'shift', + 'unshift', + 'splice', + 'sort', + 'reverse' +].forEach(function (method) { + // cache original method + var original = arrayProto[method]; + def(arrayMethods, method, function mutator () { + var args = [], len = arguments.length; + while ( len-- ) args[ len ] = arguments[ len ]; + + var result = original.apply(this, args); + var ob = this.__ob__; + var inserted; + switch (method) { + case 'push': + case 'unshift': + inserted = args; + break + case 'splice': + inserted = args.slice(2); + break + } + if (inserted) { ob.observeArray(inserted); } + // notify change + ob.dep.notify(); + return result + }); +}); + +/* */ + +var arrayKeys = Object.getOwnPropertyNames(arrayMethods); + +/** + * By default, when a reactive property is set, the new value is + * also converted to become reactive. However when passing down props, + * we don't want to force conversion because the value may be a nested value + * under a frozen data structure. Converting it would defeat the optimization. + */ +var observerState = { + shouldConvert: true +}; + +/** + * Observer class that are attached to each observed + * object. Once attached, the observer converts target + * object's property keys into getter/setters that + * collect dependencies and dispatches updates. + */ +var Observer = function Observer (value) { + this.value = value; + this.dep = new Dep(); + this.vmCount = 0; + def(value, '__ob__', this); + if (Array.isArray(value)) { + var augment = hasProto + ? protoAugment + : copyAugment; + augment(value, arrayMethods, arrayKeys); + this.observeArray(value); + } else { + this.walk(value); + } +}; + +/** + * Walk through each property and convert them into + * getter/setters. This method should only be called when + * value type is Object. + */ +Observer.prototype.walk = function walk (obj) { + var keys = Object.keys(obj); + for (var i = 0; i < keys.length; i++) { + defineReactive(obj, keys[i], obj[keys[i]]); + } +}; + +/** + * Observe a list of Array items. + */ +Observer.prototype.observeArray = function observeArray (items) { + for (var i = 0, l = items.length; i < l; i++) { + observe(items[i]); + } +}; + +// helpers + +/** + * Augment an target Object or Array by intercepting + * the prototype chain using __proto__ + */ +function protoAugment (target, src, keys) { + /* eslint-disable no-proto */ + target.__proto__ = src; + /* eslint-enable no-proto */ +} + +/** + * Augment an target Object or Array by defining + * hidden properties. + */ +/* istanbul ignore next */ +function copyAugment (target, src, keys) { + for (var i = 0, l = keys.length; i < l; i++) { + var key = keys[i]; + def(target, key, src[key]); + } +} + +/** + * Attempt to create an observer instance for a value, + * returns the new observer if successfully observed, + * or the existing observer if the value already has one. + */ +function observe (value, asRootData) { + if (!isObject(value) || value instanceof VNode) { + return + } + var ob; + if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { + ob = value.__ob__; + } else if ( + observerState.shouldConvert && + !isServerRendering() && + (Array.isArray(value) || isPlainObject(value)) && + Object.isExtensible(value) && + !value._isVue + ) { + ob = new Observer(value); + } + if (asRootData && ob) { + ob.vmCount++; + } + return ob +} + +/** + * Define a reactive property on an Object. + */ +function defineReactive ( + obj, + key, + val, + customSetter, + shallow +) { + var dep = new Dep(); + + var property = Object.getOwnPropertyDescriptor(obj, key); + if (property && property.configurable === false) { + return + } + + // cater for pre-defined getter/setters + var getter = property && property.get; + var setter = property && property.set; + + var childOb = !shallow && observe(val); + Object.defineProperty(obj, key, { + enumerable: true, + configurable: true, + get: function reactiveGetter () { + var value = getter ? getter.call(obj) : val; + if (Dep.target) { + dep.depend(); + if (childOb) { + childOb.dep.depend(); + if (Array.isArray(value)) { + dependArray(value); + } + } + } + return value + }, + set: function reactiveSetter (newVal) { + var value = getter ? getter.call(obj) : val; + /* eslint-disable no-self-compare */ + if (newVal === value || (newVal !== newVal && value !== value)) { + return + } + /* eslint-enable no-self-compare */ + if ("development" !== 'production' && customSetter) { + customSetter(); + } + if (setter) { + setter.call(obj, newVal); + } else { + val = newVal; + } + childOb = !shallow && observe(newVal); + dep.notify(); + } + }); +} + +/** + * Set a property on an object. Adds the new property and + * triggers change notification if the property doesn't + * already exist. + */ +function set (target, key, val) { + if (Array.isArray(target) && isValidArrayIndex(key)) { + target.length = Math.max(target.length, key); + target.splice(key, 1, val); + return val + } + if (key in target && !(key in Object.prototype)) { + target[key] = val; + return val + } + var ob = (target).__ob__; + if (target._isVue || (ob && ob.vmCount)) { + "development" !== 'production' && warn( + 'Avoid adding reactive properties to a Vue instance or its root $data ' + + 'at runtime - declare it upfront in the data option.' + ); + return val + } + if (!ob) { + target[key] = val; + return val + } + defineReactive(ob.value, key, val); + ob.dep.notify(); + return val +} + +/** + * Delete a property and trigger change if necessary. + */ +function del (target, key) { + if (Array.isArray(target) && isValidArrayIndex(key)) { + target.splice(key, 1); + return + } + var ob = (target).__ob__; + if (target._isVue || (ob && ob.vmCount)) { + "development" !== 'production' && warn( + 'Avoid deleting properties on a Vue instance or its root $data ' + + '- just set it to null.' + ); + return + } + if (!hasOwn(target, key)) { + return + } + delete target[key]; + if (!ob) { + return + } + ob.dep.notify(); +} + +/** + * Collect dependencies on array elements when the array is touched, since + * we cannot intercept array element access like property getters. + */ +function dependArray (value) { + for (var e = (void 0), i = 0, l = value.length; i < l; i++) { + e = value[i]; + e && e.__ob__ && e.__ob__.dep.depend(); + if (Array.isArray(e)) { + dependArray(e); + } + } +} + +/* */ + +/** + * Option overwriting strategies are functions that handle + * how to merge a parent option value and a child option + * value into the final value. + */ +var strats = config.optionMergeStrategies; + +/** + * Options with restrictions + */ +{ + strats.el = strats.propsData = function (parent, child, vm, key) { + if (!vm) { + warn( + "option \"" + key + "\" can only be used during instance " + + 'creation with the `new` keyword.' + ); + } + return defaultStrat(parent, child) + }; +} + +/** + * Helper that recursively merges two data objects together. + */ +function mergeData (to, from) { + if (!from) { return to } + var key, toVal, fromVal; + var keys = Object.keys(from); + for (var i = 0; i < keys.length; i++) { + key = keys[i]; + toVal = to[key]; + fromVal = from[key]; + if (!hasOwn(to, key)) { + set(to, key, fromVal); + } else if (isPlainObject(toVal) && isPlainObject(fromVal)) { + mergeData(toVal, fromVal); + } + } + return to +} + +/** + * Data + */ +function mergeDataOrFn ( + parentVal, + childVal, + vm +) { + if (!vm) { + // in a Vue.extend merge, both should be functions + if (!childVal) { + return parentVal + } + if (!parentVal) { + return childVal + } + // when parentVal & childVal are both present, + // we need to return a function that returns the + // merged result of both functions... no need to + // check if parentVal is a function here because + // it has to be a function to pass previous merges. + return function mergedDataFn () { + return mergeData( + typeof childVal === 'function' ? childVal.call(this, this) : childVal, + typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal + ) + } + } else { + return function mergedInstanceDataFn () { + // instance merge + var instanceData = typeof childVal === 'function' + ? childVal.call(vm, vm) + : childVal; + var defaultData = typeof parentVal === 'function' + ? parentVal.call(vm, vm) + : parentVal; + if (instanceData) { + return mergeData(instanceData, defaultData) + } else { + return defaultData + } + } + } +} + +strats.data = function ( + parentVal, + childVal, + vm +) { + if (!vm) { + if (childVal && typeof childVal !== 'function') { + "development" !== 'production' && warn( + 'The "data" option should be a function ' + + 'that returns a per-instance value in component ' + + 'definitions.', + vm + ); + + return parentVal + } + return mergeDataOrFn(parentVal, childVal) + } + + return mergeDataOrFn(parentVal, childVal, vm) +}; + +/** + * Hooks and props are merged as arrays. + */ +function mergeHook ( + parentVal, + childVal +) { + return childVal + ? parentVal + ? parentVal.concat(childVal) + : Array.isArray(childVal) + ? childVal + : [childVal] + : parentVal +} + +LIFECYCLE_HOOKS.forEach(function (hook) { + strats[hook] = mergeHook; +}); + +/** + * Assets + * + * When a vm is present (instance creation), we need to do + * a three-way merge between constructor options, instance + * options and parent options. + */ +function mergeAssets ( + parentVal, + childVal, + vm, + key +) { + var res = Object.create(parentVal || null); + if (childVal) { + "development" !== 'production' && assertObjectType(key, childVal, vm); + return extend(res, childVal) + } else { + return res + } +} + +ASSET_TYPES.forEach(function (type) { + strats[type + 's'] = mergeAssets; +}); + +/** + * Watchers. + * + * Watchers hashes should not overwrite one + * another, so we merge them as arrays. + */ +strats.watch = function ( + parentVal, + childVal, + vm, + key +) { + // work around Firefox's Object.prototype.watch... + if (parentVal === nativeWatch) { parentVal = undefined; } + if (childVal === nativeWatch) { childVal = undefined; } + /* istanbul ignore if */ + if (!childVal) { return Object.create(parentVal || null) } + { + assertObjectType(key, childVal, vm); + } + if (!parentVal) { return childVal } + var ret = {}; + extend(ret, parentVal); + for (var key$1 in childVal) { + var parent = ret[key$1]; + var child = childVal[key$1]; + if (parent && !Array.isArray(parent)) { + parent = [parent]; + } + ret[key$1] = parent + ? parent.concat(child) + : Array.isArray(child) ? child : [child]; + } + return ret +}; + +/** + * Other object hashes. + */ +strats.props = +strats.methods = +strats.inject = +strats.computed = function ( + parentVal, + childVal, + vm, + key +) { + if (childVal && "development" !== 'production') { + assertObjectType(key, childVal, vm); + } + if (!parentVal) { return childVal } + var ret = Object.create(null); + extend(ret, parentVal); + if (childVal) { extend(ret, childVal); } + return ret +}; +strats.provide = mergeDataOrFn; + +/** + * Default strategy. + */ +var defaultStrat = function (parentVal, childVal) { + return childVal === undefined + ? parentVal + : childVal +}; + +/** + * Validate component names + */ +function checkComponents (options) { + for (var key in options.components) { + validateComponentName(key); + } +} + +function validateComponentName (name) { + if (!/^[a-zA-Z][\w-]*$/.test(name)) { + warn( + 'Invalid component name: "' + name + '". Component names ' + + 'can only contain alphanumeric characters and the hyphen, ' + + 'and must start with a letter.' + ); + } + if (isBuiltInTag(name) || config.isReservedTag(name)) { + warn( + 'Do not use built-in or reserved HTML elements as component ' + + 'id: ' + name + ); + } +} + +/** + * Ensure all props option syntax are normalized into the + * Object-based format. + */ +function normalizeProps (options, vm) { + var props = options.props; + if (!props) { return } + var res = {}; + var i, val, name; + if (Array.isArray(props)) { + i = props.length; + while (i--) { + val = props[i]; + if (typeof val === 'string') { + name = camelize(val); + res[name] = { type: null }; + } else { + warn('props must be strings when using array syntax.'); + } + } + } else if (isPlainObject(props)) { + for (var key in props) { + val = props[key]; + name = camelize(key); + res[name] = isPlainObject(val) + ? val + : { type: val }; + } + } else { + warn( + "Invalid value for option \"props\": expected an Array or an Object, " + + "but got " + (toRawType(props)) + ".", + vm + ); + } + options.props = res; +} + +/** + * Normalize all injections into Object-based format + */ +function normalizeInject (options, vm) { + var inject = options.inject; + if (!inject) { return } + var normalized = options.inject = {}; + if (Array.isArray(inject)) { + for (var i = 0; i < inject.length; i++) { + normalized[inject[i]] = { from: inject[i] }; + } + } else if (isPlainObject(inject)) { + for (var key in inject) { + var val = inject[key]; + normalized[key] = isPlainObject(val) + ? extend({ from: key }, val) + : { from: val }; + } + } else { + warn( + "Invalid value for option \"inject\": expected an Array or an Object, " + + "but got " + (toRawType(inject)) + ".", + vm + ); + } +} + +/** + * Normalize raw function directives into object format. + */ +function normalizeDirectives (options) { + var dirs = options.directives; + if (dirs) { + for (var key in dirs) { + var def = dirs[key]; + if (typeof def === 'function') { + dirs[key] = { bind: def, update: def }; + } + } + } +} + +function assertObjectType (name, value, vm) { + if (!isPlainObject(value)) { + warn( + "Invalid value for option \"" + name + "\": expected an Object, " + + "but got " + (toRawType(value)) + ".", + vm + ); + } +} + +/** + * Merge two option objects into a new one. + * Core utility used in both instantiation and inheritance. + */ +function mergeOptions ( + parent, + child, + vm +) { + { + checkComponents(child); + } + + if (typeof child === 'function') { + child = child.options; + } + + normalizeProps(child, vm); + normalizeInject(child, vm); + normalizeDirectives(child); + var extendsFrom = child.extends; + if (extendsFrom) { + parent = mergeOptions(parent, extendsFrom, vm); + } + if (child.mixins) { + for (var i = 0, l = child.mixins.length; i < l; i++) { + parent = mergeOptions(parent, child.mixins[i], vm); + } + } + var options = {}; + var key; + for (key in parent) { + mergeField(key); + } + for (key in child) { + if (!hasOwn(parent, key)) { + mergeField(key); + } + } + function mergeField (key) { + var strat = strats[key] || defaultStrat; + options[key] = strat(parent[key], child[key], vm, key); + } + return options +} + +/** + * Resolve an asset. + * This function is used because child instances need access + * to assets defined in its ancestor chain. + */ +function resolveAsset ( + options, + type, + id, + warnMissing +) { + /* istanbul ignore if */ + if (typeof id !== 'string') { + return + } + var assets = options[type]; + // check local registration variations first + if (hasOwn(assets, id)) { return assets[id] } + var camelizedId = camelize(id); + if (hasOwn(assets, camelizedId)) { return assets[camelizedId] } + var PascalCaseId = capitalize(camelizedId); + if (hasOwn(assets, PascalCaseId)) { return assets[PascalCaseId] } + // fallback to prototype chain + var res = assets[id] || assets[camelizedId] || assets[PascalCaseId]; + if ("development" !== 'production' && warnMissing && !res) { + warn( + 'Failed to resolve ' + type.slice(0, -1) + ': ' + id, + options + ); + } + return res +} + +/* */ + +function validateProp ( + key, + propOptions, + propsData, + vm +) { + var prop = propOptions[key]; + var absent = !hasOwn(propsData, key); + var value = propsData[key]; + // handle boolean props + if (isType(Boolean, prop.type)) { + if (absent && !hasOwn(prop, 'default')) { + value = false; + } else if (!isType(String, prop.type) && (value === '' || value === hyphenate(key))) { + value = true; + } + } + // check default value + if (value === undefined) { + value = getPropDefaultValue(vm, prop, key); + // since the default value is a fresh copy, + // make sure to observe it. + var prevShouldConvert = observerState.shouldConvert; + observerState.shouldConvert = true; + observe(value); + observerState.shouldConvert = prevShouldConvert; + } + { + assertProp(prop, key, value, vm, absent); + } + return value +} + +/** + * Get the default value of a prop. + */ +function getPropDefaultValue (vm, prop, key) { + // no default, return undefined + if (!hasOwn(prop, 'default')) { + return undefined + } + var def = prop.default; + // warn against non-factory defaults for Object & Array + if ("development" !== 'production' && isObject(def)) { + warn( + 'Invalid default value for prop "' + key + '": ' + + 'Props with type Object/Array must use a factory function ' + + 'to return the default value.', + vm + ); + } + // the raw prop value was also undefined from previous render, + // return previous default value to avoid unnecessary watcher trigger + if (vm && vm.$options.propsData && + vm.$options.propsData[key] === undefined && + vm._props[key] !== undefined + ) { + return vm._props[key] + } + // call factory function for non-Function types + // a value is Function if its prototype is function even across different execution context + return typeof def === 'function' && getType(prop.type) !== 'Function' + ? def.call(vm) + : def +} + +/** + * Assert whether a prop is valid. + */ +function assertProp ( + prop, + name, + value, + vm, + absent +) { + if (prop.required && absent) { + warn( + 'Missing required prop: "' + name + '"', + vm + ); + return + } + if (value == null && !prop.required) { + return + } + var type = prop.type; + var valid = !type || type === true; + var expectedTypes = []; + if (type) { + if (!Array.isArray(type)) { + type = [type]; + } + for (var i = 0; i < type.length && !valid; i++) { + var assertedType = assertType(value, type[i]); + expectedTypes.push(assertedType.expectedType || ''); + valid = assertedType.valid; + } + } + if (!valid) { + warn( + "Invalid prop: type check failed for prop \"" + name + "\"." + + " Expected " + (expectedTypes.map(capitalize).join(', ')) + + ", got " + (toRawType(value)) + ".", + vm + ); + return + } + var validator = prop.validator; + if (validator) { + if (!validator(value)) { + warn( + 'Invalid prop: custom validator check failed for prop "' + name + '".', + vm + ); + } + } +} + +var simpleCheckRE = /^(String|Number|Boolean|Function|Symbol)$/; + +function assertType (value, type) { + var valid; + var expectedType = getType(type); + if (simpleCheckRE.test(expectedType)) { + var t = typeof value; + valid = t === expectedType.toLowerCase(); + // for primitive wrapper objects + if (!valid && t === 'object') { + valid = value instanceof type; + } + } else if (expectedType === 'Object') { + valid = isPlainObject(value); + } else if (expectedType === 'Array') { + valid = Array.isArray(value); + } else { + valid = value instanceof type; + } + return { + valid: valid, + expectedType: expectedType + } +} + +/** + * Use function string name to check built-in types, + * because a simple equality check will fail when running + * across different vms / iframes. + */ +function getType (fn) { + var match = fn && fn.toString().match(/^\s*function (\w+)/); + return match ? match[1] : '' +} + +function isType (type, fn) { + if (!Array.isArray(fn)) { + return getType(fn) === getType(type) + } + for (var i = 0, len = fn.length; i < len; i++) { + if (getType(fn[i]) === getType(type)) { + return true + } + } + /* istanbul ignore next */ + return false +} + +/* */ + +function handleError (err, vm, info) { + if (vm) { + var cur = vm; + while ((cur = cur.$parent)) { + var hooks = cur.$options.errorCaptured; + if (hooks) { + for (var i = 0; i < hooks.length; i++) { + try { + var capture = hooks[i].call(cur, err, vm, info) === false; + if (capture) { return } + } catch (e) { + globalHandleError(e, cur, 'errorCaptured hook'); + } + } + } + } + } + globalHandleError(err, vm, info); +} + +function globalHandleError (err, vm, info) { + if (config.errorHandler) { + try { + return config.errorHandler.call(null, err, vm, info) + } catch (e) { + logError(e, null, 'config.errorHandler'); + } + } + logError(err, vm, info); +} + +function logError (err, vm, info) { + { + warn(("Error in " + info + ": \"" + (err.toString()) + "\""), vm); + } + /* istanbul ignore else */ + if ((inBrowser || inWeex) && typeof console !== 'undefined') { + console.error(err); + } else { + throw err + } +} + +/* */ +/* globals MessageChannel */ + +var callbacks = []; +var pending = false; + +function flushCallbacks () { + pending = false; + var copies = callbacks.slice(0); + callbacks.length = 0; + for (var i = 0; i < copies.length; i++) { + copies[i](); + } +} + +// Here we have async deferring wrappers using both micro and macro tasks. +// In < 2.4 we used micro tasks everywhere, but there are some scenarios where +// micro tasks have too high a priority and fires in between supposedly +// sequential events (e.g. #4521, #6690) or even between bubbling of the same +// event (#6566). However, using macro tasks everywhere also has subtle problems +// when state is changed right before repaint (e.g. #6813, out-in transitions). +// Here we use micro task by default, but expose a way to force macro task when +// needed (e.g. in event handlers attached by v-on). +var microTimerFunc; +var macroTimerFunc; +var useMacroTask = false; + +// Determine (macro) Task defer implementation. +// Technically setImmediate should be the ideal choice, but it's only available +// in IE. The only polyfill that consistently queues the callback after all DOM +// events triggered in the same loop is by using MessageChannel. +/* istanbul ignore if */ +if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { + macroTimerFunc = function () { + setImmediate(flushCallbacks); + }; +} else if (typeof MessageChannel !== 'undefined' && ( + isNative(MessageChannel) || + // PhantomJS + MessageChannel.toString() === '[object MessageChannelConstructor]' +)) { + var channel = new MessageChannel(); + var port = channel.port2; + channel.port1.onmessage = flushCallbacks; + macroTimerFunc = function () { + port.postMessage(1); + }; +} else { + /* istanbul ignore next */ + macroTimerFunc = function () { + setTimeout(flushCallbacks, 0); + }; +} + +// Determine MicroTask defer implementation. +/* istanbul ignore next, $flow-disable-line */ +if (typeof Promise !== 'undefined' && isNative(Promise)) { + var p = Promise.resolve(); + microTimerFunc = function () { + p.then(flushCallbacks); + // in problematic UIWebViews, Promise.then doesn't completely break, but + // it can get stuck in a weird state where callbacks are pushed into the + // microtask queue but the queue isn't being flushed, until the browser + // needs to do some other work, e.g. handle a timer. Therefore we can + // "force" the microtask queue to be flushed by adding an empty timer. + if (isIOS) { setTimeout(noop); } + }; +} else { + // fallback to macro + microTimerFunc = macroTimerFunc; +} + +/** + * Wrap a function so that if any code inside triggers state change, + * the changes are queued using a Task instead of a MicroTask. + */ +function withMacroTask (fn) { + return fn._withTask || (fn._withTask = function () { + useMacroTask = true; + var res = fn.apply(null, arguments); + useMacroTask = false; + return res + }) +} + +function nextTick (cb, ctx) { + var _resolve; + callbacks.push(function () { + if (cb) { + try { + cb.call(ctx); + } catch (e) { + handleError(e, ctx, 'nextTick'); + } + } else if (_resolve) { + _resolve(ctx); + } + }); + if (!pending) { + pending = true; + if (useMacroTask) { + macroTimerFunc(); + } else { + microTimerFunc(); + } + } + // $flow-disable-line + if (!cb && typeof Promise !== 'undefined') { + return new Promise(function (resolve) { + _resolve = resolve; + }) + } +} + +/* */ + +var mark; +var measure; + +{ + var perf = inBrowser && window.performance; + /* istanbul ignore if */ + if ( + perf && + perf.mark && + perf.measure && + perf.clearMarks && + perf.clearMeasures + ) { + mark = function (tag) { return perf.mark(tag); }; + measure = function (name, startTag, endTag) { + perf.measure(name, startTag, endTag); + perf.clearMarks(startTag); + perf.clearMarks(endTag); + perf.clearMeasures(name); + }; + } +} + +/* not type checking this file because flow doesn't play well with Proxy */ + +var initProxy; + +{ + var allowedGlobals = makeMap( + 'Infinity,undefined,NaN,isFinite,isNaN,' + + 'parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,' + + 'Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,' + + 'require' // for Webpack/Browserify + ); + + var warnNonPresent = function (target, key) { + warn( + "Property or method \"" + key + "\" is not defined on the instance but " + + 'referenced during render. Make sure that this property is reactive, ' + + 'either in the data option, or for class-based components, by ' + + 'initializing the property. ' + + 'See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.', + target + ); + }; + + var hasProxy = + typeof Proxy !== 'undefined' && + Proxy.toString().match(/native code/); + + if (hasProxy) { + var isBuiltInModifier = makeMap('stop,prevent,self,ctrl,shift,alt,meta,exact'); + config.keyCodes = new Proxy(config.keyCodes, { + set: function set (target, key, value) { + if (isBuiltInModifier(key)) { + warn(("Avoid overwriting built-in modifier in config.keyCodes: ." + key)); + return false + } else { + target[key] = value; + return true + } + } + }); + } + + var hasHandler = { + has: function has (target, key) { + var has = key in target; + var isAllowed = allowedGlobals(key) || key.charAt(0) === '_'; + if (!has && !isAllowed) { + warnNonPresent(target, key); + } + return has || !isAllowed + } + }; + + var getHandler = { + get: function get (target, key) { + if (typeof key === 'string' && !(key in target)) { + warnNonPresent(target, key); + } + return target[key] + } + }; + + initProxy = function initProxy (vm) { + if (hasProxy) { + // determine which proxy handler to use + var options = vm.$options; + var handlers = options.render && options.render._withStripped + ? getHandler + : hasHandler; + vm._renderProxy = new Proxy(vm, handlers); + } else { + vm._renderProxy = vm; + } + }; +} + +/* */ + +var seenObjects = new _Set(); + +/** + * Recursively traverse an object to evoke all converted + * getters, so that every nested property inside the object + * is collected as a "deep" dependency. + */ +function traverse (val) { + _traverse(val, seenObjects); + seenObjects.clear(); +} + +function _traverse (val, seen) { + var i, keys; + var isA = Array.isArray(val); + if ((!isA && !isObject(val)) || Object.isFrozen(val)) { + return + } + if (val.__ob__) { + var depId = val.__ob__.dep.id; + if (seen.has(depId)) { + return + } + seen.add(depId); + } + if (isA) { + i = val.length; + while (i--) { _traverse(val[i], seen); } + } else { + keys = Object.keys(val); + i = keys.length; + while (i--) { _traverse(val[keys[i]], seen); } + } +} + +/* */ + +var normalizeEvent = cached(function (name) { + var passive = name.charAt(0) === '&'; + name = passive ? name.slice(1) : name; + var once$$1 = name.charAt(0) === '~'; // Prefixed last, checked first + name = once$$1 ? name.slice(1) : name; + var capture = name.charAt(0) === '!'; + name = capture ? name.slice(1) : name; + return { + name: name, + once: once$$1, + capture: capture, + passive: passive + } +}); + +function createFnInvoker (fns) { + function invoker () { + var arguments$1 = arguments; + + var fns = invoker.fns; + if (Array.isArray(fns)) { + var cloned = fns.slice(); + for (var i = 0; i < cloned.length; i++) { + cloned[i].apply(null, arguments$1); + } + } else { + // return handler return value for single handlers + return fns.apply(null, arguments) + } + } + invoker.fns = fns; + return invoker +} + +function updateListeners ( + on, + oldOn, + add, + remove$$1, + vm +) { + var name, def, cur, old, event; + for (name in on) { + def = cur = on[name]; + old = oldOn[name]; + event = normalizeEvent(name); + /* istanbul ignore if */ + if (isUndef(cur)) { + "development" !== 'production' && warn( + "Invalid handler for event \"" + (event.name) + "\": got " + String(cur), + vm + ); + } else if (isUndef(old)) { + if (isUndef(cur.fns)) { + cur = on[name] = createFnInvoker(cur); + } + add(event.name, cur, event.once, event.capture, event.passive, event.params); + } else if (cur !== old) { + old.fns = cur; + on[name] = old; + } + } + for (name in oldOn) { + if (isUndef(on[name])) { + event = normalizeEvent(name); + remove$$1(event.name, oldOn[name], event.capture); + } + } +} + +/* */ + +function mergeVNodeHook (def, hookKey, hook) { + if (def instanceof VNode) { + def = def.data.hook || (def.data.hook = {}); + } + var invoker; + var oldHook = def[hookKey]; + + function wrappedHook () { + hook.apply(this, arguments); + // important: remove merged hook to ensure it's called only once + // and prevent memory leak + remove(invoker.fns, wrappedHook); + } + + if (isUndef(oldHook)) { + // no existing hook + invoker = createFnInvoker([wrappedHook]); + } else { + /* istanbul ignore if */ + if (isDef(oldHook.fns) && isTrue(oldHook.merged)) { + // already a merged invoker + invoker = oldHook; + invoker.fns.push(wrappedHook); + } else { + // existing plain hook + invoker = createFnInvoker([oldHook, wrappedHook]); + } + } + + invoker.merged = true; + def[hookKey] = invoker; +} + +/* */ + +function extractPropsFromVNodeData ( + data, + Ctor, + tag +) { + // we are only extracting raw values here. + // validation and default values are handled in the child + // component itself. + var propOptions = Ctor.options.props; + if (isUndef(propOptions)) { + return + } + var res = {}; + var attrs = data.attrs; + var props = data.props; + if (isDef(attrs) || isDef(props)) { + for (var key in propOptions) { + var altKey = hyphenate(key); + { + var keyInLowerCase = key.toLowerCase(); + if ( + key !== keyInLowerCase && + attrs && hasOwn(attrs, keyInLowerCase) + ) { + tip( + "Prop \"" + keyInLowerCase + "\" is passed to component " + + (formatComponentName(tag || Ctor)) + ", but the declared prop name is" + + " \"" + key + "\". " + + "Note that HTML attributes are case-insensitive and camelCased " + + "props need to use their kebab-case equivalents when using in-DOM " + + "templates. You should probably use \"" + altKey + "\" instead of \"" + key + "\"." + ); + } + } + checkProp(res, props, key, altKey, true) || + checkProp(res, attrs, key, altKey, false); + } + } + return res +} + +function checkProp ( + res, + hash, + key, + altKey, + preserve +) { + if (isDef(hash)) { + if (hasOwn(hash, key)) { + res[key] = hash[key]; + if (!preserve) { + delete hash[key]; + } + return true + } else if (hasOwn(hash, altKey)) { + res[key] = hash[altKey]; + if (!preserve) { + delete hash[altKey]; + } + return true + } + } + return false +} + +/* */ + +// The template compiler attempts to minimize the need for normalization by +// statically analyzing the template at compile time. +// +// For plain HTML markup, normalization can be completely skipped because the +// generated render function is guaranteed to return Array. There are +// two cases where extra normalization is needed: + +// 1. When the children contains components - because a functional component +// may return an Array instead of a single root. In this case, just a simple +// normalization is needed - if any child is an Array, we flatten the whole +// thing with Array.prototype.concat. It is guaranteed to be only 1-level deep +// because functional components already normalize their own children. +function simpleNormalizeChildren (children) { + for (var i = 0; i < children.length; i++) { + if (Array.isArray(children[i])) { + return Array.prototype.concat.apply([], children) + } + } + return children +} + +// 2. When the children contains constructs that always generated nested Arrays, +// e.g.