magenticians launcher
PulseStorm Launcher in Magento 1.X.
<?xml version="1.0"?> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../lib/internal/Magento/Framework/Module/etc/module.xsd"> <module name="Magenticians_Launcher" version="0.0.0.1" active="true" /> </config>As overheard in the Magento 2 front-end webinar, Magento 2 ships with schema defintions for recurring XML files. Note that XML files will be interpreted without referencing to these schemas, but adhering to standards — and receiving IDE hinting! — is always a good thing. Going to the Advanced > Advanced section of the system configuration, you can verify that the module has been indeed recognized by the Magento system.
<?xml version="1.0"?> <layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../Magento/Core/etc/layout_single.xsd"> <referenceContainer name="head"> <block class="Magento\Theme\Block\Html\Head\Script" name="magenticians-launcher-launcher-js"> <arguments> <argument name="file" xsi:type="string">Magenticians_Launcher::js/launcher.js</argument> </arguments> </block> <block class="Magento\Theme\Block\Html\Head\Css" name="magenticians-launcher-launcher-css"> <arguments> <argument name="file" xsi:type="string">Magenticians_Launcher::css/launcher.css</argument> </arguments> </block> </referenceContainer> </layout>Verifying that everything went correct: refresh the admin panel and check the browser its console, it should display the “I am here!” line added a tad ago.
This is where we will attempt to add the interface element.
<referenceContainer name="header"> <block class="Magento\AdminNotification\Block\ToolbarEntry" before="user" template="toolbar_entry.phtml" /> </referenceContainer>The XML instructions are straight forward and comparable to how it’s done in Magento 1.X. We only have to adapt this example and add it to the module its default.xml:
<referenceContainer name="header"> <block class="Magento\Framework\View\Element\Template" before="search" name="launcher_toolbar_entry" template="Magenticians_Launcher::toolbar_entry.phtml" /> </referenceContainer>Instead of creating an underlying block class from scratch, we will use a standard internal Magento view element which will render the template for us. Logically, the toolbar_entry.phtml goes in the templates directory. Let’s fill it in.
<div id="magenticians-launcher-toolbar"> <a id="magenticians-launcher-link" (0);">+</a> </div>And the accompanying CSS which goes in launcher.css. Note that we also prematurely add the minimal styling for the launcher its interface:
#magenticians-launcher-toolbar { display: inline-block; font-weight: bold; font-size: 31px; vertical-align: top; } #magenticians-launcher-toolbar > a { color: #fff; } #magenticians-launcher-toolbar > a:hover { color: #ccc; text-decoration: none; } #magenticians-launcher-dialog { display: none; } #magenticians-launcher-input { width: 100%; }There we go. Refreshing the admin panel should now reveal a “+”-sign to the left of the search-icon. For those paying attention, you will notice it is at the wrong side. What’s wrong?!
Note actual order of the modules might be different – ours simply isn’t on time.
<?xml version="1.0"?> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../lib/internal/Magento/Framework/Module/etc/module.xsd"> <module name="Magenticians_Launcher" version="0.0.0.1" active="true"> <sequence> <module name="Magento_Backend" /> </sequence> </module> </config>With this change, refreshing the admin panel should now reveal the interface element on its proper position.
<?php namespace Magenticians\Launcher\Block; class LauncherItems extends \Magento\Backend\Block\Template { protected $_template = 'launcher.phtml'; }That will do the trick. To have it render on the page, we will have to add an entry to layout/default.xml:
[...] <referenceContainer name="before_body_end"> <block class="Magenticians\Launcher\Block\LauncherItems" name="launcher_items" /> </referenceContainer> </layout>Because the position of the dialog is irrelevant, we’ll throw the block in the before_body_end container. That container its location shouldn’t be a surprise. Just like before, we will not use anything fancy interface-wise. Just a simple text input should do the trick:
** Magenticians/Launcher/view/adminhtml/templates/launcher.phtml ** <?php /** * @var $this Magenticians\Launcher\Block\LauncherItems */?> <script type="text/javascript"> var launcher_items = {}; </script> <div id="magenticians-launcher-dialog" title="Launcher"> <input placeholder="Awaiting command." type="text" id="magenticians-launcher-input" /> </div>As you can see we have defined a global launcher_items variable. Empty for now, it will soon contain all the entries which the launcher will use.
jQuery(document).ready(function($) { $('#magenticians-launcher-link').click(function() { $('#magenticians-launcher-dialog').dialog({ width: 500 }); }); }Going back to the admin panel and clicking the “+” in the tray, the dialog which we defined will greet you. Because the input field in the dialog doesn’t do anything yet, let’s continue. To make the input field work, we will use jQuery UI Autocomplete. It’s really simple:
$('#magenticians-launcher-input').autocomplete({ source: launcher_items });Because the launcher_items we defined in launcher.phtml is still empty, let’s tinker about the best format we can define the entries in. Looking at the documentation there are quite a few types the source option supports. The most fitting one for us, is using an array of objects with label and value properties. This way we can define the labels of the entries we can search through and use the value to store the URL that entry “launches”:
$('#magenticians-launcher-input').autocomplete({ source: launcher_items, // On selecting an entry, we point the document to the location attached to it select: function(event, ui) { event.preventDefault(); document.location = $(this).attr('data-target'); }, // When an entry receives focus we display the label in the input and store the URL in "data-target" focus: function(event, ui) { event.preventDefault(); $(this).val(ui.item.label); $(this).attr('data-target', ui.item.value); }, // When the autocomplete widget is initialized, we quickly remove the accessible helper as we don't need it create: function(event) { $(this).next('.ui-helper-hidden-accessible').remove(); } });To test it out, simply define an array in the specified format and temporarily add it to launcher.html its launcher_items:
var launcher_items = [{label: "Homepage", value: "https://magenticians.com"}, {label: "@Magenticians on Twitter", value: "http://twitter.com/magenticians"}, {label: "Get in touch", value: "https://magenticians.com/contact"}];
The launcher at work with some dummy data.
** Magento/Backend/view/adminhtml/templates/menu.phtml ** <nav class="navigation"> <?php echo $this->renderNavigation($this->getMenuModel(), 0, 12); ?> </nav>The renderNavigation method, as its name implies, renders the navigation. It’s a bit nasty that a lot of HTML is being generated within a core class, but we will look over that. It does however show us how we can obtain the menu entries:
/** @var $menuItem \Magento\Backend\Model\Menu\Item */foreach ($this->_getMenuIterator($menu) as $menuItem) { [...]The iterator is retrieved from the iterator factory:
protected function _getMenuIterator($menu) { return $this->_iteratorFactory->create(array('iterator' => $menu->getIterator())); }$menu is an instance of Magento\Backend\Model\Menu. The renderNavigation method is initially fed the root menu of the current menu configuration ($this->getMenuModel() in menu.phtml). Then, it gets iterated over recursively. Most of the Menu block class its methods are protected. Though we can repurpose a few of the methods (like $this->getMenuModel()), some of the code has to be duplicated to the LauncherItems block. You could choose to extend the Menu-block, but as Menu and LauncherItems are completely different, it wouldn’t really make hierarchical sense. Editing the mostly empty LauncherItems block class created a while ago, we will start with defining the dependencies. Magento will take care of injecting them for us:
<?php namespace Magenticians\Launcher\Block; class LauncherItems extends \Magento\Backend\Block\Template { protected $_template = 'launcher.phtml'; protected $_iteratorFactory; protected $_blockMenu; public function __construct( \Magento\Backend\Block\Template\Context $context, \Magento\Backend\Model\Menu\Filter\IteratorFactory $iteratorFactory, \Magento\Backend\Block\Menu $blockMenu, array $data ) { $this->_iteratorFactory = $iteratorFactory; $this->_blockMenu = $blockMenu; $this->_url = $url; parent::__construct($context, $data); } ]Now we have the dependencies available, we can reuse the Menu block its getMenuModel() method:
public function getMenuModel() { return $this->_blockMenu->getMenuModel(); }Because the _getMenuIterator method is protected, we have to redefine it in our class:
protected function getMenuIterator($menu) { return $this->_iteratorFactory->create(array('iterator' => $menu->getIterator())); }Now we have those two methods in place, we can recursively iterate over the menu. Don’t be spooked by the recursion part, it’s a lot simpler than it sounds: we will start iterating over the root elements. Then, if a root item has children, we will iterate over its children. And so forth. It’s not that complex. We will call the method which does this getMenuArray:
public function getMenuArray($menu, & $result = array()) { foreach ($this->getMenuIterator($menu) as $menuItem) { /** @var $menuItem \Magento\Backend\Model\Menu\Item */ $result[] = array( 'value' => $menuItem->getUrl(), 'label' => $fullName . $menuItem->getTitle() ); if ($menuItem->hasChildren()) { $this->getMenuArray($menuItem->getChildren(), $result); } } return $result; }Because we keep passing a reference of the initial array, we can add all the entries accordingly. There are two problems we need to fix, though. First of all, the labels we get, will not be very meaningful. In the original PulseStorm Launcher sub menus were separated by a dash. Let’s replicate that:
const ITEMS_SEPARATOR = ' - '; public function getMenuArray($menu, & $result = array(), $fullName = '') { if (! empty($fullName)) { $fullName .= self::ITEMS_SEPARATOR; } foreach ($this->getMenuIterator($menu) as $menuItem) { /** @var $menuItem \Magento\Backend\Model\Menu\Item */ $result[] = array( 'value' => $menuItem->getUrl(), 'label' => $fullName . $menuItem->getTitle() ); if ($menuItem->hasChildren()) { $this->getMenuArray($menuItem->getChildren(), $result, $fullName . $menuItem->getTitle()); } } return $result; }The second problem is that some of the menu entries are meaningless in a launcher because they are merely textual; they don’t have an associated URL. We will skip those:
if ($menuItem->getUrl() !== '#') { // Only add meaningful entries $result[] = array( 'value' => $menuItem->getUrl(), 'label' => $fullName . $menuItem->getTitle() ); }For those still spooked by the recursion part, here’s an example of a run:
public function getMenuJson() { $menuArray = $this->getMenuArray($this->getMenuModel()); return json_encode($menuArray); }And the accompanying update to launcher.phtml:
<script type="text/javascript"> var launcher_items = <?php echo $this->getMenuJson(); ?>; </script>
protected function _afterToHtml($html) { $html = preg_replace_callback( '#' . \Magento\Backend\Model\UrlInterface::SECRET_KEY_PARAM_NAME . '/\$([^\/].*)/([^\/].*)/([^\$].*)\$#U', array($this, '_callbackSecretKey'), $html ); return $html; } protected function _callbackSecretKey($match) { return \Magento\Backend\Model\UrlInterface::SECRET_KEY_PARAM_NAME . '/' . $this->_url->getSecretKey( $match[1], $match[2], $match[3] ); }The absolute final change we need to make is to add the JSON_UNESCAPED_SLASHES flag-parameter to json_encode in getMenuJson(). Otherwise, the RegEx used will break. Refresh the admin panel for the last time and boot the launcher to see it at work.
Your greatest possible competitive advantage can be your clients and the interactions they have with… Read More
Digital marketing KPIs are measurable values that marketing teams use to track progress toward desired… Read More
In today's digital age, fraud poses a significant threat to businesses of all sizes. As… Read More
Financial crimes continue to evolve and proliferate in our increasingly digital, global economy. From complex… Read More
In the highly competitive modern workplace, trust, and employee loyalty are crucial factors for long-term… Read More
In the ever-evolving world of small business developing and implementing effective marketing strategies is critical to… Read More
View Comments
Thank you so much. Hope you continue sharing Magento 2 based articles.
Thank you for your post. With version dev 88, we need to change a little bit to make it works.n1/ In module.xml : Change version to schema_versionnn2/ Move csslauncher.css and jslauncher.js to pubstaticadminhtmlMagentobackendn_USMagenticians_Launcher
Thanks for this. Can you please tell, where we can find MagentoThemeBlockHtmlHeadScript from default.xml now? The path does not exist anymore, could not find the current path yet. Thanks!
you need to download the last code version from github. The way of adding JS & CSS files has been changed and adapted to current version of Magentwo !
Thank you for your example.
I just download and testes the source code from github it works fine ! , but I have one question :
Why In your helper and a block you redeclare a dependency for UrlInterface using a new property "protected $_url" ?
you could have used $this ->_urlBuilder->getUrl(...) instead of $this->_url->getUrl(...)
Hello. Thanks for this, very useful. Do you know how to uninstall this module using the cli
I have tried ....
bin/magento module:uninstall
but this leaves an entry in the setup_module table.
thanks