瀏覽代碼

Merge branch 'master' of https://github.com/unaio/una

AntonLV 5 年之前
父節點
當前提交
47eb19e405
共有 27 個文件被更改,包括 1445 次插入 和 9 次删除
  1. 7 0
      inc/classes/BxDolMenuQuery.php
  2. 2 2
      inc/classes/BxDolModule.php
  3. 20 0
      modules/boonex/help_tours/classes/BxHelpToursConfig.php
  4. 150 0
      modules/boonex/help_tours/classes/BxHelpToursDb.php
  5. 369 0
      modules/boonex/help_tours/classes/BxHelpToursGridItems.php
  6. 237 0
      modules/boonex/help_tours/classes/BxHelpToursGridTours.php
  7. 46 0
      modules/boonex/help_tours/classes/BxHelpToursModule.php
  8. 47 0
      modules/boonex/help_tours/classes/BxHelpToursStudioPage.php
  9. 62 0
      modules/boonex/help_tours/classes/BxHelpToursTemplate.php
  10. 71 0
      modules/boonex/help_tours/install/config.php
  11. 20 0
      modules/boonex/help_tours/install/installer.php
  12. 52 0
      modules/boonex/help_tours/install/langs/en.xml
  13. 4 0
      modules/boonex/help_tours/install/sql/disable.sql
  14. 38 0
      modules/boonex/help_tours/install/sql/enable.sql
  15. 48 0
      modules/boonex/help_tours/install/sql/install.sql
  16. 17 0
      modules/boonex/help_tours/install/sql/uninstall.sql
  17. 47 0
      modules/boonex/help_tours/js/grid_items.js
  18. 16 0
      modules/boonex/help_tours/request.php
  19. 6 0
      modules/boonex/help_tours/template/css/admin.css
  20. 10 0
      modules/boonex/help_tours/template/grid_items.html
  21. 27 0
      modules/boonex/help_tours/template/images/icons/std-icon.svg
  22. 70 0
      modules/boonex/help_tours/template/tour.html
  23. 12 0
      modules/boonex/nexus/classes/BxNexusModule.php
  24. 2 0
      modules/boonex/nexus/install/langs/en.xml
  25. 2 1
      modules/boonex/nexus/install/sql/enable.sql
  26. 11 6
      modules/boonex/nexus/template/js.html
  27. 52 0
      modules/boonex/russian/data/langs/bx_help_tours/ru.xml

+ 7 - 0
inc/classes/BxDolMenuQuery.php

@@ -34,6 +34,13 @@ class BxDolMenuQuery extends BxDolDb
         return $aObject;
     }
 
+    static public function getMenuObjects($bActive = true, $bFromCache = true)
+    {
+        $oDb = BxDolDb::getInstance();
+        $sSql = $oDb->prepare("SELECT `sys_objects_menu`.`title`, `object`, `module`, `uri` FROM `sys_objects_menu` INNER JOIN `sys_modules` ON (`sys_modules`.`name` = `sys_objects_menu`.`module`) WHERE `active` = ? ORDER BY FIELD(`module`, 'system') DESC, `module` ASC, `object` ASC", $bActive ? 1 : 0);
+        return $bFromCache ? $oDb->fromMemory('sys_menus', 'getAll', $sSql) : $oDb->getAll($sSql);
+    }
+
     static public function getMenuTriggers($sTriggerName)
     {
         $oDb = BxDolDb::getInstance();

+ 2 - 2
inc/classes/BxDolModule.php

@@ -111,13 +111,13 @@ class BxDolModule extends BxDol
     }
 
     public static function getTitle($sUri)
-    {
+    {   
         return _t(self::getTitleKey($sUri));
     }
 
     public static function getTitleKey($sUri)
     {
-        return '_sys_module_' . strtolower(str_replace(' ', '_', $sUri));
+        return 'system' == $sUri ? '_adm_txt_module_system' : '_sys_module_' . strtolower(str_replace(' ', '_', $sUri));
     }
 
 	/**

+ 20 - 0
modules/boonex/help_tours/classes/BxHelpToursConfig.php

@@ -0,0 +1,20 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    Help Tours Help Tours
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+class BxHelpToursConfig extends BxBaseModGeneralConfig
+{
+    public function __construct($aModule)
+    {
+        parent::__construct($aModule);
+    }
+}
+
+/** @} */

+ 150 - 0
modules/boonex/help_tours/classes/BxHelpToursDb.php

@@ -0,0 +1,150 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    Help Tours Help Tours
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+class BxHelpToursDb extends BxDolModuleDb
+{
+    private $_sTourBlockTemplate = 'a:3:{s:6:"module";s:13:"bx_help_tours";s:6:"method";s:13:"get_help_tour";s:6:"params";a:1:{i:0;i:__tour_id__;}}';
+    private $_sBlockSelectorTemplate = '#bx-page-block-__block_id__ > div';
+
+    public function __construct(&$oConfig)
+    {
+        parent::__construct($oConfig);
+    }
+
+    public function getTourDetails($iTourId) {
+        return $this->getRow("SELECT * FROM `bx_help_tours` WHERE `id` = :id", ['id' => $iTourId]);
+    }
+
+    public function getTourPageUrl($iTourId) {
+        $aTour = $this->getTourDetails($iTourId);
+        if (!$aTour) return false;
+
+        if ($aTour['page'] == 'sys_home') return '/';
+
+        return $this->getOne("SELECT `url` FROM `sys_objects_page` WHERE `object` = :page", ['page' => $aTour['page']]);
+    }
+
+    public function getSitePages() {
+        $aPages = $this->getAll("SELECT `object`, `module`, `title_system` AS `name`  FROM `sys_objects_page` WHERE `title_system` <> '' ORDER BY `title_system` ASC");
+        $aPagesByModules = [];
+        foreach ($aPages as $aPage) {
+            $aPagesByModules[$aPage['module']][$aPage['object']] = _t($aPage['name']);
+        }
+
+        $aResult = [];
+        foreach ($aPagesByModules as $sModule => $aPages) {
+            $oModule = BxDolModule::getInstance($sModule);
+            $sTitle = $oModule && isset($oModule->_aModule) ? $oModule->_aModule['title'] : ucfirst($sModule);
+
+            $aResult[$sModule.'_start'] = ['type' => 'group_header', 'value' => $sTitle];
+            $aResult = $aResult + $aPages;
+            $aResult[$sModule.'_end'] = ['type' => 'group_end'];
+        }
+
+        return $aResult;
+    }
+
+    public function deleteHelpTourItems($iEntry) {
+        $this->query("DELETE FROM `bx_help_tours_items` WHERE `tour` = :tour", ['tour' => $iEntry]);
+    }
+
+    public function getPageTitleDetails($sPageObject) {
+        $aData = $this->getRow("SELECT `module`, `title_system` FROM `sys_objects_page` WHERE `object` = :page", ['page' => $sPageObject]);
+        if (!$aData) return false;
+
+        $oModule = BxDolModule::getInstance($aData['module']);
+        $aData['module_title'] = $oModule && isset($oModule->_aModule) ? $oModule->_aModule['title'] : ucfirst($aData['module']);
+
+        return $aData;
+    }
+
+    public function putHelpTourOnPage($iTourId, $sOldPage) {
+        $aTour = $this->getTourDetails($iTourId);
+        $sPage = $aTour['page'];
+
+        if ($sOldPage != $sPage) {
+            if (!empty($sOldPage)) {
+                $this->query("DELETE FROM `sys_pages_blocks` WHERE `object` = :page AND `module` = 'bx_help_tours'", [
+                    'page' => $sOldPage,
+                ]);
+            }
+
+            $iOrder = intval($this->getOne("SELECT MAX(`order`) FROM `sys_pages_blocks` WHERE `object` = :page and `cell_id` = 1", ['page' => $sPage])) + 1;
+
+            $this->query("
+                INSERT INTO `sys_pages_blocks` (`object`, `cell_id`, `module`, `title`, `designbox_id`, `visible_for_levels`, `type`, `content`, `deletable`, `copyable`, `order`) VALUES
+                (:page, 1, 'bx_help_tours', '_bx_help_tours_page_block_caption', 0, 2147483647, 'service', :service_call, 0, 0, :order);
+            ", [
+                'page' => $sPage,
+                'service_call' => str_replace('__tour_id__', $iTourId, $this->_sTourBlockTemplate),
+                'order' => $iOrder,
+            ]);
+        }
+    }
+
+    function deleteHelpTourBlock($iTourId) {
+        $aTour = $this->getTourDetails($iTourId);
+
+        $this->query("DELETE FROM `sys_pages_blocks` WHERE `module` = 'bx_help_tours' AND `object` = :page", [
+            'page' => $aTour['page'],
+        ]);
+    }
+
+    function getHelpTourItemsCount($iTourId) {
+        return $this->getOne("SELECT COUNT(*) FROM `bx_help_tours_items` WHERE `tour` = :tour", [
+            'tour' => $iTourId,
+        ]);
+    }
+
+    public function getHelpTourItems($iTourId) {
+        return $this->getAll("SELECT * FROM `bx_help_tours_items` WHERE `tour` = :tour ORDER BY `order` ASC", [
+            'tour' => $iTourId,
+        ]);
+    }
+
+    public function getTourItemDetails($iItemId) {
+        return $this->getRow("SELECT * FROM `bx_help_tours_items` WHERE `id` = :id", ['id' => $iItemId]);
+    }
+
+    public function getPageBlocksForHelpTour($iTourId) {
+        $aTour = $this->getTourDetails($iTourId);
+        if (!$aTour) return [];
+
+        $aResult = [];
+
+        $aRes = $this->getAll("SELECT `id`, `title_system`, `title` FROM `sys_pages_blocks` WHERE `object` = :page AND `cell_id` > 0 AND `active` AND `module` <> 'bx_help_tours' AND (`title_system` <> '' OR `title` <> '') ORDER BY `cell_id` ASC, `order` ASC", ['page' => $aTour['page']]);
+        if ($aRes) {
+            foreach ($aRes as $aRow) {
+                $aResult[str_replace('__block_id__', $aRow['id'], $this->_sBlockSelectorTemplate)] = !empty($aRow['title_system']) ? _t($aRow['title_system']) : _t($aRow['title']);
+            }
+        }
+
+        return $aResult;
+    }
+
+    public function getHelpToursOptions() {
+        return $this->getPairs("SELECT `id`, `page` FROM `bx_help_tours` ORDER BY `order` ASC", 'id', 'page');
+    }
+
+    public function deleteAccountData($iAccount) {
+        $this->query("DELETE FROM `bx_help_tours_track_views` WHERE `account` = :account", ['account' => $iAccount]);
+    }
+
+    public function trackTourSeen($iAccount, $iTour) {
+        $this->query("INSERT IGNORE INTO `bx_help_tours_track_views` (`account`, `tour`) VALUES(:account, :tour)", ['account' => $iAccount, 'tour' => $iTour]);
+    }
+
+    public function isHelpTourSeen($iAccount, $iTour) {
+        return $this->getOne("SELECT COUNT(*) FROM `bx_help_tours_track_views` WHERE `account` = :account AND `tour` = :tour", ['account' => $iAccount, 'tour' => $iTour]);
+    }
+}
+
+/** @} */

+ 369 - 0
modules/boonex/help_tours/classes/BxHelpToursGridItems.php

@@ -0,0 +1,369 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    Help Tours Help Tours
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+class BxHelpToursGridItems extends BxTemplGrid
+{
+    protected $_iTourId;
+    protected $_aTourPageBlocks;
+
+    public function __construct ($aOptions, $oTemplate = false)
+    {
+        $this->MODULE = 'bx_help_tours';
+    	$this->_oModule = BxDolModule::getInstance($this->MODULE);
+    	if(!$oTemplate)
+			$oTemplate = $this->_oModule->_oTemplate;
+
+    	parent::__construct ($aOptions, $oTemplate);
+
+    	$this->_iTourId = bx_get('tour');
+        if ($this->_iTourId) {
+            $this->_aQueryAppend['tour'] = $this->_iTourId;
+            $this->_aTourPageBlocks = $this->_oModule->_oDb->getPageBlocksForHelpTour($this->_iTourId);
+        }
+    }
+
+    protected function _getDataSql($sFilter, $sOrderField, $sOrderDir, $iStart, $iPerPage)
+    {
+        if(!empty($this->_iTourId)) {
+            $this->_aOptions['source'] .= $this->_oModule->_oDb->prepareAsString(" AND `tour` = ? ", $this->_iTourId);
+            return parent::_getDataSql($sFilter, $sOrderField, $sOrderDir, $iStart, $iPerPage);
+        }
+        return false;
+    }
+
+    protected function _getCellElement($mixedValue, $sKey, $aField, $aRow)
+    {
+        if (isset($this->_aTourPageBlocks[$aRow['element']])) $mixedValue = htmlspecialchars($this->_aTourPageBlocks[$aRow['element']]);
+        return parent::_getCellDefault($mixedValue, $sKey, $aField, $aRow);
+    }
+
+    private function getAddEditForm($iEntry) {
+        $aEntry = false;
+
+        if ($iEntry) {
+            $aEntry = $this->_oModule->_oDb->getTourItemDetails($iEntry);
+            if (!$aEntry) {
+                echoJson([]);
+                exit;
+            }
+        }
+
+        $oForm = $this->getAddEditFormObject($iEntry);
+
+        $oForm->aFormAttrs['action'] = BX_DOL_URL_ROOT.'grid.php?o=' . $this->_sObject . '&a='.($aEntry ? 'edit' : 'add');
+
+        if ($aEntry) {
+            $aEntry['element_helper'] = $aEntry['element'];
+            if ($aEntry['element'] && !isset($this->_aTourPageBlocks[$aEntry['element']])) $aEntry['element_helper'] = 'custom';
+        }
+        $oForm->initChecker($aEntry);
+        if (isset($oForm->aInputs['element_helper']['value']) && $oForm->aInputs['element_helper']['value'] == 'custom')
+            unset($oForm->aInputs['element']['tr_attrs']);
+
+        if ($oForm->isSubmittedAndValid()) {
+            if ($aEntry) {
+                $oForm->update($aEntry['id']);
+                $iResult = $aEntry['id'];
+            } else {
+                $iResult = $oForm->insert();
+            }
+
+            if ($iResult) {
+                $aRes = ['grid' => $this->getCode(false), 'blink' => $iResult];
+            } else {
+                $aRes = ['msg' => "Error occured"]; // if record adding failed, display error message
+            }
+            echoJson($aRes);
+        } else {
+            $s = '
+                <div class="bx-def-color-bg-block">' . $oForm->getCode(true) . '</div>                
+                <script>                    
+                    $(document).ready(function () {
+                        $("#'.$oForm->aFormAttrs['id'].'").ajaxForm({
+                            dataType: "json",
+                            beforeSubmit: function (formData, jqForm, options) {
+                                bx_loading($("#' . $oForm->aFormAttrs['id'] . '"), true);
+                            },
+                            success: function (data) {
+                                $(".bx-popup-active:visible").dolPopupHide();
+                                glGrids.' . $this->_sObject . '.processJson(data, "'.($aEntry ? 'edit' : 'add').'");
+                            }
+                        });
+                    });
+                </script>';
+
+            echoJson(['popup' => [
+                'html' => BxTemplStudioFunctions::getInstance()->popupBox('bx-tour-item-details-popup', _t('_bx_help_tours_cpt_tour_item_details'), $s),
+                'options' => [
+                    'onBeforeShow' => empty($aEntry) ? "$('#grid-popup-bx_help_tours_items-edit').remove();" : "$('#grid-popup-bx_help_tours_items-add').remove();",
+                    'closeOnOuterClick' => false,
+                ]]]
+            );
+        }
+    }
+
+    public function performActionAdd()
+    {
+        if(!$this->canAdd()) {
+            echoJson([]);
+            exit;
+        }
+
+        return $this->getAddEditForm(0);
+    }
+
+    public function performActionEdit()
+    {
+        $iEntry = !is_null($_REQUEST) && isset($_REQUEST['id']) ? intval($_REQUEST['id']) : intval($_REQUEST['ids'][0]);
+        if(!$iEntry) {
+            echoJson([]);
+            exit;
+        }
+
+        return $this->getAddEditForm($iEntry);
+    }
+
+    private function getAddEditFormObject($iEntry) {
+        $aArrowOptionsSrc = ['auto', 'auto-start', 'auto-end', 'top', 'top-start', 'top-end', 'bottom', 'bottom-start', 'bottom-end', 'right', 'right-start', 'right-end', 'left', 'left-start', 'left-end'];
+        $aArrowOptions = ['' => _t('_bx_help_tours_arrow_option_none')];
+        foreach ($aArrowOptionsSrc as $sOption) {
+            $aArrowOptions[$sOption] = $sOption;
+        }
+
+        $aForm = [
+            'form_attrs' => [
+                'method' => 'post',
+                'action' => '',
+                'id' => 'bx-help-tour-item-details-form',
+            ],
+            'params' => [
+                'db' => [
+                    'submit_name' => 'submit',
+                    'table' => 'bx_help_tours_items',
+                    'key' => 'id',
+                    'uri' => '',
+                    'uri_title' => '',
+                ],
+            ],
+            'inputs' => [
+                'id' => [
+                    'type' => 'hidden',
+                    'name' => 'id',
+                    'value' => $iEntry,
+                ],
+                'tour' => [
+                    'type' => 'hidden',
+                    'name' => 'tour',
+                    'value' => $this->_iTourId,
+                    'db' => [
+                        'pass' => 'Int',
+                    ],
+                ],
+                'name' => [
+                    'type' => 'text',
+                    'name' => 'name',
+                    'required' => true,
+                    'caption' => _t('_bx_help_tours_form_field_name'),
+                    'info' => _t('_bx_help_tours_form_field_name_info'),
+                    'checker' => [
+                        'func' => 'Avail',
+                        'error' => _t('_bx_help_tours_form_field_name_error'),
+                    ],
+                    'db' => [
+                        'pass' => 'Xss',
+                    ],
+                ],
+                'element_helper' => [
+                    'type' => 'select',
+                    'name' => 'element_helper',
+                    'caption' => _t('_bx_help_tours_form_field_element'),
+                    'info' => _t('_bx_help_tours_form_field_element_info'),
+                    'values' =>
+                        ['' => _t('_bx_help_tours_form_field_option_none')] +
+                        $this->_aTourPageBlocks +
+                        ['custom' => _t('_bx_help_tours_form_field_option_custom')],
+                    'attrs' => [
+                        'onchange' => "
+                            $('#bx-help-tours-input-element').val(this.value == 'custom' ? '' : this.value);
+                            if (this.value == 'custom') $('#bx-form-element-element').fadeIn(); else $('#bx-form-element-element').fadeOut();                            
+                         ",
+                    ],
+                ],
+                'element' => [
+                    'type' => 'text',
+                    'name' => 'element',
+                    'caption' => _t('_bx_help_tours_form_field_element_custom'),
+                    'info' => _t('_bx_help_tours_form_field_element_custom_info'),
+                    'attrs' => [
+                        'id' => 'bx-help-tours-input-element',
+                    ],
+                    'tr_attrs' => [
+                        'style' => 'display:none;',
+                    ],
+                    'db' => array (
+                        'pass' => 'Xss',
+                    ),
+                ],
+                'title' => [
+                    'type' => 'text_translatable',
+                    'name' => 'title',
+                    'caption' => _t('_bx_help_tours_form_field_title'),
+                    'db' => [
+                        'pass' => 'Xss',
+                    ],
+                ],
+                'text' => [
+                    'type' => 'textarea_translatable',
+                    'name' => 'text',
+                    'required' => true,
+                    'caption' => _t('_bx_help_tours_form_field_text'),
+                    'checker' => [
+                        'func' => 'AvailTranslatable',
+                        'params' => ['text'],
+                        'error' => _t('_bx_help_tours_form_field_text_error'),
+                    ],
+                    'db' => [
+                        'pass' => 'Xss',
+                    ],
+                    'code' => true, //to avoid loosing HTML formatting
+                ],
+                'arrow' => [
+                    'type' => 'select',
+                    'name' => 'arrow',
+                    'caption' => _t('_bx_help_tours_form_field_arrow'),
+                    'values' => $aArrowOptions,
+                    'value' => 'auto',
+                    'db' => array (
+                        'pass' => 'Xss',
+                    ),
+                ],
+                'actions' => [
+                    'type' => 'input_set',
+                    0 => [
+                        'type' => 'submit',
+                        'name' => 'submit',
+                        'value' => _t('_Submit'),
+                    ],
+                    1 => [
+                        'type' => 'button',
+                        'name' => 'cancel',
+                        'value' => _t('_Cancel'),
+                        'attrs' => [
+                            'style' => 'margin-left: 20px;',
+                            'onclick' => '$(".bx-popup-active:visible").dolPopupHide();',
+                        ],
+                    ],
+                ],
+            ],
+        ];
+        return new BxTemplStudioFormView($aForm);
+    }
+
+    public function performActionDelete() {
+        $aIds = bx_get('ids');
+        if (!$aIds || !is_array($aIds)) {
+            echoJson(array());
+            exit;
+        }
+
+        $oLanguage = BxDolStudioLanguagesUtils::getInstance();
+
+        foreach ($aIds as $iEntry) {
+            $aItem = $this->_oModule->_oDb->getTourItemDetails($iEntry);
+            $oLanguage->deleteLanguageString($aItem['title']);
+            $oLanguage->deleteLanguageString($aItem['text']);
+            $this->_delete($iEntry);
+        }
+
+        echoJson(array(
+            'grid' => $this->getCode(false),
+        ));
+    }
+
+    public function _getActionPreview($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = array()) {
+        $sUrl = $this->_oModule->_oDb->getTourPageUrl($this->_iTourId);
+        if (!$sUrl) return '';
+        $sUrl = trim($sUrl, '/');
+
+        $a['attr']['href'] = BX_DOL_URL_ROOT.bx_append_url_params(BxDolPermalinks::getInstance()->permalink($sUrl), ['help_tour_preview' => $this->_iTourId, 'help_tour_item' => $aRow['id']]);
+        $a['attr']['target'] = '_blank';
+
+        $sButton = $this->_getActionDefault($sType, $sKey, $a, $isSmall, $isDisabled, $aRow);
+        $sA = str_replace('button', 'a', $sButton);
+        return $sA;
+    }
+
+    protected function canAdd()
+    {
+        return !empty($this->_iTourId);
+    }
+
+    protected function _getActionAdd($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = array())
+    {
+        if(!$this->canAdd())
+            $isDisabled = true;
+
+        return parent::_getActionDefault($sType, $sKey, $a, $isSmall, $isDisabled, $aRow);
+    }
+
+    protected function _getFilterControls ()
+    {
+        $aTourOptions = $this->_oModule->_oDb->getHelpToursOptions();
+        if ($aTourOptions) foreach ($aTourOptions as $iTour => $sPage) {
+            $aPageTitle = $this->_oModule->_oDb->getPageTitleDetails($sPage);
+            if ($aPageTitle)
+                $aTourOptions[$iTour] = $aPageTitle['module_title'].': '._t($aPageTitle['title_system']);
+        }
+
+        $aInputTour = [
+            'type' => 'select',
+            'name' => 'tour',
+            'value' => $this->_iTourId,
+            'values' => ['' => _t('_sys_please_select')] + $aTourOptions,
+            'attrs' => array(
+                'id' => 'bx-grid-tour',
+                'onChange' => 'javascript:' . $this->getJsObject() . '.onChangeTour(this.value);',
+            ),
+        ];
+
+        $oForm = new BxTemplStudioFormView(array());
+        return $oForm->genRow($aInputTour).parent::_getFilterControls();
+    }
+
+    protected function _addJsCss()
+    {
+        parent::_addJsCss();
+        $this->_oModule->_oTemplate->addStudioCss('admin.css');
+        $this->_oModule->_oTemplate->addStudioJs('grid_items.js');
+
+        $oForm = new BxTemplStudioFormView(array());
+        $oForm->addCssJs();
+    }
+
+    function getJsObject()
+    {
+        return 'oBxHelpToursGridItems';
+    }
+
+    function getCode($isDisplayHeader = true)
+    {
+        return $this->_oModule->_oTemplate->parseHtmlByName('grid_items.html', [
+            'module_url' => BX_DOL_URL_ROOT.$this->_oModule->_oConfig->getBaseUri(),
+            'content' => parent::getCode($isDisplayHeader),
+            'js_object' => $this->getJsObject(),
+            'grid_object' => $this->_sObject,
+            'tour' => $this->_iTourId,
+            'page_url' => BX_DOL_URL_STUDIO.'module.php?name='.$this->MODULE.'&page=items',
+        ]);
+    }
+}
+
+/** @} */

+ 237 - 0
modules/boonex/help_tours/classes/BxHelpToursGridTours.php

@@ -0,0 +1,237 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    Help Tours Help Tours
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+class BxHelpToursGridTours extends BxTemplGrid
+{
+    public function __construct ($aOptions, $oTemplate = false)
+    {
+        $this->MODULE = 'bx_help_tours';
+    	$this->_oModule = BxDolModule::getInstance($this->MODULE);
+    	if(!$oTemplate)
+			$oTemplate = $this->_oModule->_oTemplate;
+
+        parent::__construct ($aOptions, $oTemplate);
+    }
+    protected function _getCellPage($mixedValue, $sKey, $aField, $aRow)
+    {
+        $aPageTitle = $this->_oModule->_oDb->getPageTitleDetails($mixedValue);
+        if ($aPageTitle)
+            $mixedValue = $aPageTitle['module_title'].': '._t($aPageTitle['title_system']);
+
+        return parent::_getCellDefault($mixedValue, $sKey, $aField, $aRow);
+    }
+
+    protected function _getCellItems($mixedValue, $sKey, $aField, $aRow)
+    {
+        $sLink = 'javascript: glGrids.'.$this->_sObject.'.action(\'edit_items\', {id: '.$aRow['id'].'});';
+        $mixedValue = $this->_oTemplate->parseLink($sLink, _t('_bx_help_tours_cpt_items_cnt', $this->_oModule->_oDb->getHelpTourItemsCount($aRow['id'])));
+        return parent::_getCellDefault($mixedValue, $sKey, $aField, $aRow);
+    }
+
+    public function _getActionPreview($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = array()) {
+        $sUrl = $this->_oModule->_oDb->getTourPageUrl($aRow['id']);
+        if (!$sUrl) return '';
+        $sUrl = trim($sUrl, '/');
+
+        $a['attr']['href'] = BX_DOL_URL_ROOT.bx_append_url_params(BxDolPermalinks::getInstance()->permalink($sUrl), ['help_tour_preview' => $aRow['id']]);
+        $a['attr']['target'] = '_blank';
+
+        $sButton = $this->_getActionDefault($sType, $sKey, $a, $isSmall, $isDisabled, $aRow);
+        $sA = str_replace('button', 'a', $sButton);
+        return $sA;
+    }
+
+    private function getAddEditForm($iEntry) {
+        $aEntry = false;
+
+        if ($iEntry) {
+            $aEntry = $this->_oModule->_oDb->getTourDetails($iEntry);
+            if (!$aEntry) {
+                echoJson([]);
+                exit;
+            }
+        }
+
+        $oForm = $this->getAddEditFormObject($iEntry);
+
+        $oForm->aFormAttrs['action'] = BX_DOL_URL_ROOT.'grid.php?o=' . $this->_sObject . '&a='.($aEntry ? 'edit' : 'add');
+
+        $oForm->initChecker($aEntry);
+
+        if ($oForm->isSubmittedAndValid()) {
+            if ($aEntry) {
+                $oForm->update($aEntry['id']);
+                $iResult = $aEntry['id'];
+            } else {
+                $iResult = $oForm->insert();
+            }
+
+            if ($iResult) {
+                $this->_oModule->_oDb->putHelpTourOnPage($iResult, $aEntry ? $aEntry['page'] : '');
+                $aRes = ['grid' => $this->getCode(false), 'blink' => $iResult];
+            } else {
+                $aRes = ['msg' => "Error occured"]; // if record adding failed, display error message
+            }
+            echoJson($aRes);
+        } else {
+            $s = '
+                <div class="bx-def-color-bg-block">' . $oForm->getCode(true) . '</div>                
+                <script>                    
+                    $(document).ready(function () {
+                        $("#'.$oForm->aFormAttrs['id'].'").ajaxForm({
+                            dataType: "json",
+                            beforeSubmit: function (formData, jqForm, options) {
+                                bx_loading($("#' . $oForm->aFormAttrs['id'] . '"), true);
+                            },
+                            success: function (data) {
+                                $(".bx-popup-active:visible").dolPopupHide();
+                                glGrids.' . $this->_sObject . '.processJson(data, "'.($aEntry ? 'edit' : 'add').'");
+                            }
+                        });
+                    });
+                </script>';
+
+            echoJson(['popup' => [
+                'html' => BxTemplStudioFunctions::getInstance()->popupBox('bx-tour-details-popup', _t('_bx_help_tours_cpt_tour_details'), $s),
+                'options' => [
+                    'closeOnOuterClick' => false,
+                ]]]
+            );
+        }
+    }
+
+    public function performActionAdd()
+    {
+        return $this->getAddEditForm(0);
+    }
+
+    public function performActionEdit()
+    {
+        $iEntry = !is_null($_REQUEST) && isset($_REQUEST['id']) ? intval($_REQUEST['id']) : intval($_REQUEST['ids'][0]);
+        if(!$iEntry) {
+            echoJson([]);
+            exit;
+        }
+
+        return $this->getAddEditForm($iEntry);
+    }
+
+    public function performActionEditItems()
+    {
+        $iTourId = !is_null($_REQUEST) && isset($_REQUEST['id']) ? intval($_REQUEST['id']) : intval($_REQUEST['ids'][0]);
+
+        if (!$iTourId) {
+            echoJson(array());
+            exit;
+        }
+
+        echoJson(array(
+            'redirect' => BX_DOL_URL_STUDIO.'module.php?name='.$this->MODULE.'&tour='.$iTourId.'&page=items',
+        ));
+    }
+
+    public function performActionDelete() {
+        $aIds = bx_get('ids');
+        if (!$aIds || !is_array($aIds)) {
+            echoJson(array());
+            exit;
+        }
+
+        $oLanguage = BxDolStudioLanguagesUtils::getInstance();
+
+        foreach ($aIds as $iEntry) {
+            $this->_oModule->_oDb->deleteHelpTourBlock($iEntry);
+
+            $aItems = $this->_oModule->_oDb->getHelpTourItems($iEntry);
+            if ($aItems) foreach ($aItems as $aItem) {
+                $oLanguage->deleteLanguageString($aItem['title']);
+                $oLanguage->deleteLanguageString($aItem['text']);
+            }
+
+            $this->_oModule->_oDb->deleteHelpTourItems($iEntry);
+            $this->_delete($iEntry);
+        }
+
+        echoJson(array(
+            'grid' => $this->getCode(false),
+        ));
+    }
+
+    private function getAddEditFormObject($iEntry) {
+        $aForm = [
+            'form_attrs' => [
+                'method' => 'post',
+                'action' => '',
+                'id' => 'bx-help-tour-details-form',
+            ],
+            'params' => [
+                'db' => [
+                    'submit_name' => 'submit',
+                    'table' => 'bx_help_tours',
+                    'key' => 'id',
+                    'uri' => '',
+                    'uri_title' => '',
+                ],
+            ],
+            'inputs' => [
+                'id' => [
+                    'type' => 'hidden',
+                    'name' => 'id',
+                    'value' => $iEntry,
+                ],
+                'page' => [
+                    'type' => 'select',
+                    'name' => 'page',
+                    'required' => true,
+                    'caption' => _t('_bx_help_tours_form_field_page'),
+                    'values' => ['' => _t('_sys_please_select')] + $this->_oModule->_oDb->getSitePages(),
+                    'checker' => [
+                        'func' => 'Avail',
+                        'error' => _t('_bx_help_tours_form_field_page_error'),
+                    ],
+                    'db' => [
+                        'pass' => 'Xss',
+                    ],
+                ],
+                'overlay' => [
+                    'type' => 'switcher',
+                    'name' => 'overlay',
+                    'caption' => _t('_bx_help_tours_form_field_overlay'),
+                    'info' => _t('_bx_help_tours_form_field_overlay_info'),
+                    'value' => 1,
+                    'db' => [
+                        'pass' => 'Int',
+                    ],
+                ],
+                'actions' => [
+                    'type' => 'input_set',
+                    0 => [
+                        'type' => 'submit',
+                        'name' => 'submit',
+                        'value' => _t('_Submit'),
+                    ],
+                    1 => [
+                        'type' => 'button',
+                        'name' => 'cancel',
+                        'value' => _t('_Cancel'),
+                        'attrs' => [
+                            'style' => 'margin-left: 20px;',
+                            'onclick' => '$(".bx-popup-active:visible").dolPopupHide();',
+                        ],
+                    ],
+                ],
+            ],
+        ];
+        return new BxTemplFormView($aForm);
+    }
+}
+
+/** @} */

+ 46 - 0
modules/boonex/help_tours/classes/BxHelpToursModule.php

@@ -0,0 +1,46 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    Help Tours Help Tours
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+class BxHelpToursModule extends BxDolModule
+{
+    public function __construct(&$aModule)
+    {
+        parent::__construct($aModule);
+    }
+
+    public function serviceGetHelpTour($iTourId) {
+        if (!isLogged() || !$iTourId) return;
+
+        $aTour = $this->_oDb->getTourDetails($iTourId);
+        if (!$aTour) return;
+
+        $aHelpTourItems = $this->_oDb->getHelpTourItems($iTourId);
+        if (!$aHelpTourItems) return;
+
+        if ((!isAdmin() || !bx_get('help_tour_preview')) && $this->_oDb->isHelpTourSeen(getLoggedId(), $iTourId)) return;
+
+        return $this->_oTemplate->getHelpTourCode($aTour, $aHelpTourItems);
+    }
+
+    public function serviceResponseAccountDelete($oAlert) {
+        if ('account' != $oAlert->sUnit || 'delete' != $oAlert->sAction) return;
+
+        $this->_oDb->deleteAccountData($oAlert->iObject);
+    }
+
+    public function actionTourSeen() {
+        if (!isLogged() || !$iTourId = bx_get('tour')) return;
+
+        $this->_oDb->trackTourSeen(getLoggedId(), $iTourId);
+    }
+}
+
+/** @} */

+ 47 - 0
modules/boonex/help_tours/classes/BxHelpToursStudioPage.php

@@ -0,0 +1,47 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    Help Tours Help Tours
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+class BxHelpToursStudioPage extends BxTemplStudioModule
+{
+	protected $_sModule;
+	protected $_oModule;
+
+    function __construct($sModule, $mixedPageName, $sPage = "")
+    {
+    	$this->_sModule = 'bx_help_tours';
+    	$this->_oModule = BxDolModule::getInstance($this->_sModule);
+
+        parent::__construct($sModule, $mixedPageName, $sPage);
+
+		$this->aMenuItems = array(
+            'tours' => array('name' => 'tours', 'icon' => 'info', 'title' => '_bx_help_tours_page_tours'),
+            'items' => array('name' => 'items', 'icon' => 'list', 'title' => '_bx_help_tours_page_items'),
+        );
+    }
+
+	protected function getTours()
+    {
+		bx_import('BxTemplGrid');
+        $oGrid = BxDolGrid::getObjectInstance('bx_help_tours_tours', BxDolStudioTemplate::getInstance());
+        if (!$oGrid) return 'The module have to be reinstalled';
+        return $oGrid->getCode();
+    }
+
+    protected function getItems()
+    {
+        bx_import('BxTemplGrid');
+        $oGrid = BxDolGrid::getObjectInstance('bx_help_tours_items', BxDolStudioTemplate::getInstance());
+        if (!$oGrid) return 'The module have to be reinstalled';
+        return $oGrid->getCode();
+    }
+}
+
+/** @} */

+ 62 - 0
modules/boonex/help_tours/classes/BxHelpToursTemplate.php

@@ -0,0 +1,62 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    Help Tours Help Tours
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+class BxHelpToursTemplate extends BxBaseModGeneralTemplate
+{
+    public function __construct(&$oConfig, &$oDb)
+    {
+        parent::__construct($oConfig, $oDb);
+    }
+
+    public function getHelpTourCode($aTour, $aHelpTourItems) {
+        $this->addJs('shepherd/js/shepherd.min.js');
+        $this->addCss(BX_DIRECTORY_PATH_PLUGINS_PUBLIC . 'shepherd/css/|shepherd.css');
+
+        $iShowOnlyItem = isAdmin() && bx_get('help_tour_item') ? intval(bx_get('help_tour_item')) : 0;
+
+        $aHelpTourItemsTmpl = [];
+        foreach ($aHelpTourItems as $iCounter => $aItem) {
+            if ($iShowOnlyItem && $iShowOnlyItem != $aItem['id']) continue;
+
+            $aStep = [
+                'id' => 'bx-help-tour-step-'.$aItem['id'],
+                'text' => _t($aItem['text']),
+                'attachTo' => ['element' => $aItem['element'], 'on' => $aItem['arrow']],
+            ];
+
+            if ($aItem['title']) $aStep['title'] = _t($aItem['title']);
+            if ($aItem['element']) $aStep['attachTo'] = ['element' => $aItem['element'], 'on' => $aItem['arrow']];
+
+            $aHelpTourItemsTmpl[] = $aStep;
+        }
+
+        if (!$aHelpTourItemsTmpl) return;
+
+        return $this->parseHtmlByName('tour.html', [
+            'tour_overlay' => $aTour['overlay'] ? 'true' : 'false',
+            'steps' => json_encode($aHelpTourItemsTmpl),
+            'js_obj' => $this->getTourJsObject(),
+            'bx_if:not_preview' => [
+                'condition' => !(isAdmin() && (bx_get('help_tour_preview') || bx_get('help_tour_item'))),
+                'content' => [
+                    'finish_callback_url' => BX_DOL_URL_ROOT . $this->_oConfig->getBaseUri() . 'tour_seen/',
+                    'tour_id' => $aTour['id'],
+                ],
+            ],
+        ]);
+    }
+
+    public function getTourJsObject() {
+        return 'bx_help_tour_obj';
+    }
+}
+
+/** @} */

+ 71 - 0
modules/boonex/help_tours/install/config.php

@@ -0,0 +1,71 @@
+<?php
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    Help Tours Help Tours
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+$aConfig = array(
+    /**
+     * Main Section.
+     */
+    'type' => BX_DOL_MODULE_TYPE_MODULE,
+    'name' => 'bx_help_tours',
+    'title' => 'Help Tours',
+    'note' => 'Help Tours module',
+    'version' => '12.0.2.DEV',
+    'vendor' => 'BoonEx',
+	'help_url' => 'http://feed.una.io/?section={module_name}',
+
+    'compatible_with' => array(
+        '12.0.x'
+    ),
+
+    /**
+     * 'home_dir' and 'home_uri' - should be unique. Don't use spaces in 'home_uri' and the other special chars.
+     */
+    'home_dir' => 'boonex/help_tours/',
+    'home_uri' => 'help_tours',
+
+    'db_prefix' => 'bx_help_tours_',
+    'class_prefix' => 'BxHelpTours',
+
+    /**
+     * Category for language keys.
+     */
+    'language_category' => 'Help Tours',
+
+    /**
+     * Installation/Uninstallation Section.
+     */
+    'install' => array(
+        'execute_sql' => 1,
+        'update_languages' => 1,
+        'clear_db_cache' => 1,
+    ),
+    'uninstall' => array (
+        'execute_sql' => 1,
+        'update_languages' => 1,
+        'clear_db_cache' => 1,
+    ),
+    'enable' => array(
+        'check_dependencies' => 1,
+        'execute_sql' => 1,
+        'clear_db_cache' => 1,
+    ),
+    'disable' => array(
+        'execute_sql' => 1,
+        'clear_db_cache' => 1,
+    ),
+
+    /**
+     * Dependencies Section
+     */
+    'dependencies' => array(),
+);
+
+/** @} */

+ 20 - 0
modules/boonex/help_tours/install/installer.php

@@ -0,0 +1,20 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    Help Tours Help Tours
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+class BxHelpToursInstaller extends BxDolStudioInstaller
+{
+    public function __construct($aConfig)
+    {
+        parent::__construct($aConfig);
+    }
+}
+
+/** @} */

+ 52 - 0
modules/boonex/help_tours/install/langs/en.xml

@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources name="en" flag="gb" title="English">
+	<string name="_bx_help_tours"><![CDATA[Help Tours]]></string>
+
+	<string name="_bx_help_tours_page_tours"><![CDATA[Help Tours]]></string>
+	<string name="_bx_help_tours_page_items"><![CDATA[Help Items]]></string>
+
+	<string name="_bx_help_tours_cpt_tour_details"><![CDATA[Help Tour Details]]></string>
+	<string name="_bx_help_tours_cpt_tour_item_details"><![CDATA[Help Item Details]]></string>
+	<string name="_bx_help_tours_cpt_items_cnt"><![CDATA[{0} help items]]></string>
+
+	<string name="_bx_help_tours_grid_col_page"><![CDATA[Page]]></string>
+	<string name="_bx_help_tours_grid_col_items"><![CDATA[Items]]></string>
+
+	<string name="_bx_help_tours_grid_col_name"><![CDATA[Help Item Name]]></string>
+	<string name="_bx_help_tours_grid_col_element"><![CDATA[Page block or selector]]></string>
+	<string name="_bx_help_tours_grid_col_title"><![CDATA[Help Title]]></string>
+
+	<string name="_bx_help_tours_grid_action_preview"><![CDATA[Preview Tour]]></string>
+	<string name="_bx_help_tours_grid_action_delete"><![CDATA[Delete Tour]]></string>
+	<string name="_bx_help_tours_grid_action_add"><![CDATA[Create Tour]]></string>
+	<string name="_bx_help_tours_grid_action_edit"><![CDATA[Edit Tour]]></string>
+	<string name="_bx_help_tours_grid_action_edit_items"><![CDATA[Manage Help Items]]></string>
+
+	<string name="_bx_help_tours_grid_action_preview_item"><![CDATA[Preview Item]]></string>
+	<string name="_bx_help_tours_grid_action_edit_item"><![CDATA[Edit Item]]></string>
+	<string name="_bx_help_tours_grid_action_delete_item"><![CDATA[Delete Item]]></string>
+	<string name="_bx_help_tours_grid_action_add_item"><![CDATA[Add Help Item]]></string>
+
+	<string name="_bx_help_tours_form_field_page"><![CDATA[Page]]></string>
+	<string name="_bx_help_tours_form_field_page_error"><![CDATA[The Page is required]]></string>
+	<string name="_bx_help_tours_form_field_overlay"><![CDATA[Dark Overlay]]></string>
+	<string name="_bx_help_tours_form_field_overlay_info"><![CDATA[If enabled then during showing a tour a full page dark overlay will be appearing.]]></string>
+
+	<string name="_bx_help_tours_form_field_name"><![CDATA[Name]]></string>
+	<string name="_bx_help_tours_form_field_name_info"><![CDATA[For showing in studio only]]></string>
+	<string name="_bx_help_tours_form_field_name_error"><![CDATA[The Name is required]]></string>
+	<string name="_bx_help_tours_form_field_title"><![CDATA[Help Box Title]]></string>
+	<string name="_bx_help_tours_form_field_text"><![CDATA[Help Box Text]]></string>
+	<string name="_bx_help_tours_form_field_text_error"><![CDATA[The Text is required]]></string>
+	<string name="_bx_help_tours_form_field_arrow"><![CDATA[Element's arrow position]]></string>
+	<string name="_bx_help_tours_form_field_element"><![CDATA[The page block or custom element for a Help Box]]></string>
+	<string name="_bx_help_tours_form_field_element_info"><![CDATA[If empty then the Help Box will appear centered in a page]]></string>
+	<string name="_bx_help_tours_form_field_element_custom"><![CDATA[Custom selector]]></string>
+	<string name="_bx_help_tours_form_field_element_custom_info"><![CDATA[For experienced users.<br />This may be an id, f.ex. "#bx-menu-toolbar-2-container".<br />Or a class based selector, f.ex ".bx-menu-object-sys_toolbar_member"]]></string>
+
+	<string name="_bx_help_tours_arrow_option_none"><![CDATA[None]]></string>
+	<string name="_bx_help_tours_form_field_option_none"><![CDATA[None]]></string>
+	<string name="_bx_help_tours_form_field_option_custom"><![CDATA[Custom]]></string>
+
+	<string name="_bx_help_tours_page_block_caption"><![CDATA[Help Tour]]></string>
+</resources>

+ 4 - 0
modules/boonex/help_tours/install/sql/disable.sql

@@ -0,0 +1,4 @@
+-- GRIDS
+DELETE FROM `sys_objects_grid` WHERE `object` LIKE 'bx_help_tours_%';
+DELETE FROM `sys_grid_fields` WHERE `object` LIKE 'bx_help_tours_%';
+DELETE FROM `sys_grid_actions` WHERE `object` LIKE 'bx_help_tours_%';

+ 38 - 0
modules/boonex/help_tours/install/sql/enable.sql

@@ -0,0 +1,38 @@
+-- GRID: help tours
+INSERT INTO `sys_objects_grid` (`object`, `source_type`, `source`, `table`, `field_id`, `field_order`, `field_active`, `paginate_url`, `paginate_per_page`, `paginate_simple`, `paginate_get_start`, `paginate_get_per_page`, `filter_fields`, `filter_fields_translatable`, `filter_mode`, `sorting_fields`, `sorting_fields_translatable`, `visible_for_levels`, `override_class_name`, `override_class_file`, `show_total_count`) VALUES
+('bx_help_tours_tours', 'Sql', 'SELECT * FROM `bx_help_tours` WHERE 1', 'bx_help_tours', 'id', 'order', '', '', 100, NULL, 'start', '', '', '', 'auto', '', '', 2147483647, 'BxHelpToursGridTours', 'modules/boonex/help_tours/classes/BxHelpToursGridTours.php', 0);
+
+INSERT INTO `sys_grid_fields` (`object`, `name`, `title`, `width`, `translatable`, `chars_limit`, `params`, `order`) VALUES
+('bx_help_tours_tours', 'checkbox', '', '2%', 0, 0, '', 1),
+('bx_help_tours_tours', 'order', '', '1%', 0, 0, '', 2),
+('bx_help_tours_tours', 'page', '_bx_help_tours_grid_col_page', '37%', 0, 0, '', 3),
+('bx_help_tours_tours', 'items', '_bx_help_tours_grid_col_items', '30%', 0, 0, '', 4),
+('bx_help_tours_tours', 'actions', '', '30%', 0, 0, '', 5);
+
+
+INSERT INTO `sys_grid_actions` (`object`, `type`, `name`, `title`, `icon`, `icon_only`, `confirm`, `order`) VALUES
+('bx_help_tours_tours', 'single', 'preview', '_bx_help_tours_grid_action_preview', 'eye', 1, 0, 1),
+('bx_help_tours_tours', 'single', 'edit', '_bx_help_tours_grid_action_edit', 'edit', 1, 0, 2),
+('bx_help_tours_tours', 'single', 'edit_items', '_bx_help_tours_grid_action_edit_items', 'list', 1, 0, 3),
+('bx_help_tours_tours', 'single', 'delete', '_bx_help_tours_grid_action_delete', 'remove', 1, 1, 4),
+('bx_help_tours_tours', 'bulk', 'delete', '_bx_help_tours_grid_action_delete', 'remove', 0, 1, 1),
+('bx_help_tours_tours', 'independent', 'add', '_bx_help_tours_grid_action_add', 'plus', 0, 0, 1);
+
+-- GRID: help tour items
+INSERT INTO `sys_objects_grid` (`object`, `source_type`, `source`, `table`, `field_id`, `field_order`, `field_active`, `paginate_url`, `paginate_per_page`, `paginate_simple`, `paginate_get_start`, `paginate_get_per_page`, `filter_fields`, `filter_fields_translatable`, `filter_mode`, `sorting_fields`, `sorting_fields_translatable`, `visible_for_levels`, `override_class_name`, `override_class_file`, `show_total_count`) VALUES
+('bx_help_tours_items', 'Sql', 'SELECT * FROM `bx_help_tours_items` WHERE 1', 'bx_help_tours_items', 'id', 'order', '', '', 100, NULL, 'start', '', 'element', 'title,text', 'auto', '', '', 2147483647, 'BxHelpToursGridItems', 'modules/boonex/help_tours/classes/BxHelpToursGridItems.php', 0);
+
+INSERT INTO `sys_grid_fields` (`object`, `name`, `title`, `width`, `translatable`, `chars_limit`, `params`, `order`) VALUES
+('bx_help_tours_items', 'checkbox', '', '2%', 0, 0, '', 1),
+('bx_help_tours_items', 'order', '', '1%', 0, 0, '', 2),
+('bx_help_tours_items', 'name', '_bx_help_tours_grid_col_name', '32%', 0, 0, '', 3),
+('bx_help_tours_items', 'element', '_bx_help_tours_grid_col_element', '18%', 0, 0, '', 4),
+('bx_help_tours_items', 'title', '_bx_help_tours_grid_col_title', '32%', 1, 0, '', 5),
+('bx_help_tours_items', 'actions', '', '15%', 0, 0, '', 6);
+
+INSERT INTO `sys_grid_actions` (`object`, `type`, `name`, `title`, `icon`, `icon_only`, `confirm`, `order`) VALUES
+('bx_help_tours_items', 'single', 'preview', '_bx_help_tours_grid_action_preview_item', 'eye', 1, 0, 1),
+('bx_help_tours_items', 'single', 'edit', '_bx_help_tours_grid_action_edit_item', 'edit', 1, 0, 2),
+('bx_help_tours_items', 'single', 'delete', '_bx_help_tours_grid_action_delete_item', 'remove', 1, 1, 3),
+('bx_help_tours_items', 'bulk', 'delete', '_bx_help_tours_grid_action_delete_item', 'remove', 0, 1, 1),
+('bx_help_tours_items', 'independent', 'add', '_bx_help_tours_grid_action_add_item', 'plus', 0, 0, 1);

+ 48 - 0
modules/boonex/help_tours/install/sql/install.sql

@@ -0,0 +1,48 @@
+-- Studio page and widget
+INSERT INTO `sys_std_pages`(`index`, `name`, `header`, `caption`, `icon`) VALUES
+(3, 'bx_help_tours', '_bx_help_tours', '_bx_help_tours', 'bx_help_tours@modules/boonex/help_tours/|std-icon.svg');
+SET @iPageId = LAST_INSERT_ID();
+
+SET @iParentPageId = (SELECT `id` FROM `sys_std_pages` WHERE `name` = 'home');
+SET @iParentPageOrder = (SELECT MAX(`order`) FROM `sys_std_pages_widgets` WHERE `page_id` = @iParentPageId);
+INSERT INTO `sys_std_widgets` (`page_id`, `module`, `url`, `click`, `icon`, `caption`, `cnt_notices`, `cnt_actions`) VALUES
+(@iPageId, 'bx_help_tours', '{url_studio}module.php?name=bx_help_tours&page=tours', '', 'bx_help_tours@modules/boonex/help_tours/|std-icon.svg', '_bx_help_tours', '', 'a:4:{s:6:"module";s:6:"system";s:6:"method";s:11:"get_actions";s:6:"params";a:0:{}s:5:"class";s:18:"TemplStudioModules";}');
+INSERT INTO `sys_std_pages_widgets` (`page_id`, `widget_id`, `order`) VALUES
+(@iParentPageId, LAST_INSERT_ID(), IF(ISNULL(@iParentPageOrder), 1, @iParentPageOrder + 1));
+
+-- Main Tables
+CREATE TABLE `bx_help_tours` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `overlay` tinyint(1) NOT NULL,
+  `page` varchar(128) NOT NULL,
+  `order` int(11) NOT NULL,
+  PRIMARY KEY (`id`)
+);
+
+CREATE TABLE `bx_help_tours_items` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `tour` int(11) NOT NULL,
+  `name` varchar(255) NOT NULL,
+  `element` varchar(255) NOT NULL,
+  `arrow` ENUM('auto', 'auto-start', 'auto-end', 'top', 'top-start', 'top-end', 'bottom', 'bottom-start', 'bottom-end', 'right', 'right-start', 'right-end', 'left', 'left-start', 'left-end'),
+  `title` varchar(128) NOT NULL,
+  `text` varchar(128) NOT NULL,
+  `order` int(11) NOT NULL,
+  KEY `tour` (`tour`),
+  PRIMARY KEY (`id`)
+);
+
+CREATE TABLE `bx_help_tours_track_views` (
+  `account` int(11) NOT NULL,
+  `tour` int(11) NOT NULL,
+  KEY `account` (`account`),
+  PRIMARY KEY (`account`, `tour`)
+);
+
+-- ACCOUNT REMOVAL ALERT
+INSERT INTO `sys_alerts_handlers` (`name`, `class`, `file`, `service_call`) VALUES
+('bx_help_tours_account_delete', '', '', 'a:3:{s:6:"module";s:13:"bx_help_tours";s:6:"method";s:23:"response_account_delete";s:6:"params";s:5:"$this";}');
+SET @iHandler := LAST_INSERT_ID();
+
+INSERT INTO `sys_alerts` (`unit`, `action`, `handler_id`) VALUES
+('account', 'delete', @iHandler);

+ 17 - 0
modules/boonex/help_tours/install/sql/uninstall.sql

@@ -0,0 +1,17 @@
+-- Studio page and widget
+DELETE FROM `tp`, `tw`, `tpw`
+USING `sys_std_pages` AS `tp`, `sys_std_widgets` AS `tw`, `sys_std_pages_widgets` AS `tpw`
+WHERE `tp`.`id` = `tw`.`page_id` AND `tw`.`id` = `tpw`.`widget_id` AND `tp`.`name` = 'bx_help_tours';
+
+-- Main Tables
+DROP TABLE IF EXISTS `bx_help_tours`;
+DROP TABLE IF EXISTS `bx_help_tours_items`;
+DROP TABLE IF EXISTS `bx_help_tours_track_views`;
+
+-- Help Tours page blocks
+DELETE FROM `sys_pages_blocks` WHERE `module` = 'bx_help_tours';
+
+-- ACCOUNT REMOVAL ALERT
+SET @iHandler := (SELECT `id` FROM `sys_alerts_handlers` WHERE `name` = 'bx_help_tours_account_delete' LIMIT 1);
+DELETE FROM `sys_alerts` WHERE `handler_id` = @iHandler;
+DELETE FROM `sys_alerts_handlers` WHERE `id` = @iHandler;

+ 47 - 0
modules/boonex/help_tours/js/grid_items.js

@@ -0,0 +1,47 @@
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    Help Tours Help Tours
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+function BxHelpToursGridItems(oOptions) {
+    this.sModuleUrl = oOptions.sModuleUrl;
+    this.sActionsUrl = oOptions.sActionUrl;
+    this.sPageUrl = oOptions.sPageUrl;
+    this.sObjNameGrid = oOptions.sObjNameGrid;
+    this.sObjName = oOptions.sObjName == undefined ? 'oAqbConditionalFieldsGridValues' : oOptions.sObjName;
+    this.sAnimationEffect = oOptions.sAnimationEffect == undefined ? 'fade' : oOptions.sAnimationEffect;
+    this.iAnimationSpeed = oOptions.iAnimationSpeed == undefined ? 'slow' : oOptions.iAnimationSpeed;
+
+    this.iTour = oOptions.iTour == undefined ? '' : oOptions.iTour;
+}
+
+BxHelpToursGridItems.prototype.onChangeTour = function (v) {
+    this.iTour = v;
+    this.reloadGrid();
+};
+
+BxHelpToursGridItems.prototype.reloadGrid = function () {
+    var bReload = false;
+
+    var oActions = $("[bx_grid_action_independent]");
+    if(!this.iTour){
+        oActions.addClass('bx-btn-disabled');
+    } else {
+        oActions.removeClass('bx-btn-disabled');
+    }
+
+    if (glGrids[this.sObjNameGrid]._oQueryAppend['tour'] != this.iTour) {
+        glGrids[this.sObjNameGrid]._oQueryAppend['tour'] = this.iTour;
+        bReload = true;
+    }
+
+    if (bReload) {
+        glGrids[this.sObjNameGrid].reload(0);
+    }
+};
+/** @} */

+ 16 - 0
modules/boonex/help_tours/request.php

@@ -0,0 +1,16 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    Help Tours Help Tours
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+check_logged();
+
+BxDolRequest::processAsAction($GLOBALS['aModule'], $GLOBALS['aRequest']);
+
+/** @} */

+ 6 - 0
modules/boonex/help_tours/template/css/admin.css

@@ -0,0 +1,6 @@
+div.bx-grid-header div.bx-form-element-wrapper {
+    position: relative;
+    float: left;
+    margin-top: 0px;
+    margin-right: 20px;
+}

+ 10 - 0
modules/boonex/help_tours/template/grid_items.html

@@ -0,0 +1,10 @@
+__content__
+<script language="javascript">
+    var __js_object__ = new BxHelpToursGridItems({
+        sModuleUrl: '__module_url__',
+        sActionUrl: '<bx_url_root />grid.php',
+        sPageUrl: '__page_url__',
+        sObjNameGrid: '__grid_object__',
+        iTour: '__tour__'
+    });
+</script>

+ 27 - 0
modules/boonex/help_tours/template/images/icons/std-icon.svg

@@ -0,0 +1,27 @@
+<svg width="150" height="150" viewBox="0 0 150 150" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M0 75C0 18.75 18.75 0 75 0C131.25 0 150 18.75 150 75C150 131.25 131.25 150 75 150C18.75 150 0 131.25 0 75Z" fill="url(#paint0_linear)"/>
+<g filter="url(#filter0_d)">
+<path d="M101.46 79.5407C108.165 86.246 107.442 97.5539 99.3006 103.272C94.1194 106.909 86.8823 106.909 81.7011 103.272C73.5549 97.5591 72.8368 86.246 79.5419 79.5407C85.5961 73.4864 95.4108 73.4864 101.46 79.5407" stroke="white" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M112 112L102 102" stroke="white" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M50.26 48.96C50.248 48.96 50.24 48.968 50.24 48.98C50.24 48.992 50.248 49 50.26 49C50.272 49 50.28 48.992 50.28 48.98C50.28 48.968 50.272 48.96 50.26 48.96" stroke="#F3F4F6" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M60.432 48.96C60.42 48.96 60.412 48.968 60.412 48.98C60.412 48.992 60.424 49 60.432 49C60.444 49 60.452 48.992 60.452 48.98C60.452 48.968 60.444 48.96 60.432 48.96" stroke="#F3F4F6" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M70.588 48.96C70.576 48.96 70.568 48.968 70.568 48.98C70.568 48.992 70.576 49 70.588 49C70.6 49 70.608 48.992 70.608 48.98C70.608 48.968 70.6 48.96 70.588 48.96" stroke="#F3F4F6" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M111 71V47C111 42.58 107.42 39 103 39H47C42.58 39 39 42.58 39 47V103C39 107.42 42.58 111 47 111H71" stroke="#F3F4F6" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M39.144 59H111" stroke="#F3F4F6" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
+</g>
+<defs>
+<filter id="filter0_d" x="34" y="35" width="83" height="83" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
+<feOffset dy="1"/>
+<feGaussianBlur stdDeviation="1"/>
+<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0"/>
+<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
+<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
+</filter>
+<linearGradient id="paint0_linear" x1="75" y1="0" x2="75" y2="150" gradientUnits="userSpaceOnUse">
+<stop stop-color="#10B981"/>
+<stop offset="1" stop-color="#059669"/>
+</linearGradient>
+</defs>
+</svg>

+ 70 - 0
modules/boonex/help_tours/template/tour.html

@@ -0,0 +1,70 @@
+<script defer language="javascript">
+    __js_obj__ = new Shepherd.Tour({
+        useModalOverlay: __tour_overlay__,
+        defaultStepOptions: {
+            classes: 'bx-def-z-index-modal',
+            showCancelLink: true,
+            scrollTo: {behavior: "smooth", block: "center"}
+        }
+    })
+
+    <bx_if:not_preview>
+    Shepherd.on('cancel', function () {
+        $.post('__finish_callback_url__', {tour: __tour_id__});
+    })
+    </bx_if:not_preview>
+
+    var bx_help_tour_steps =  __steps__;
+    var bx_help_tour_steps_filtered = [];
+    for (i in bx_help_tour_steps) {
+        //do not show help items for missing elements.
+        if (bx_help_tour_steps[i].attachTo.element.length && !$(bx_help_tour_steps[i].attachTo.element).length) continue;
+
+        // if this is a help for a field that is sitting inside a collapsed section then
+        // we are `uncollapsing` it on this page load
+        if (!$(bx_help_tour_steps[i].attachTo.element).is(':visible')) {
+            var sect = $(bx_help_tour_steps[i].attachTo.element).closest('.bx-form-section-hidden');
+            if (sect.length && sect.hasClass('bx-form-section-hidden')) {
+                $(sect).children('legend').trigger('click');
+                sect.removeClass('bx-form-collapsed');
+                sect.removeClass('bx-form-section-hidden');
+            }
+        }
+
+        //do not process hidden elements
+        if (!$(bx_help_tour_steps[i].attachTo.element).is(':visible')) continue;
+
+        bx_help_tour_steps_filtered.push(bx_help_tour_steps[i]);
+    }
+    bx_help_tour_steps = bx_help_tour_steps_filtered;
+
+    if (bx_help_tour_steps_filtered) {
+        var bx_help_tour_steps_active = 0;
+        for (i in bx_help_tour_steps) {
+            var aBtns = [];
+            if (i != (bx_help_tour_steps.length - 1)) {
+                aBtns.push({
+                    text: '<bx_text_js:_sys_exit />',
+                    action: __js_obj__.cancel,
+                    classes: 'shepherd-button-secondary'
+                });
+                aBtns.push({
+                    text: '<bx_text_js:_sys_next />',
+                    action: __js_obj__.next,
+                    classes: 'shepherd-button-primary'
+                });
+            } else {
+                aBtns.push({
+                    text: '<bx_text_js:_sys_done />',
+                    action: __js_obj__.cancel,
+                    classes: 'shepherd-button-primary'
+                });
+            }
+
+            bx_help_tour_steps[i].buttons = aBtns;
+            __js_obj__.addStep(bx_help_tour_steps[i]);
+            bx_help_tour_steps_active++;
+        }
+        if (bx_help_tour_steps_active) __js_obj__.start();
+    }
+</script>

+ 12 - 0
modules/boonex/nexus/classes/BxNexusModule.php

@@ -64,6 +64,7 @@ class BxNexusModule extends BxDolModule
             'txt_pull_to_refresh' => bx_js_string(_t('_bx_nexus_pull_to_refresh')),
             'txt_release_to_refresh' => bx_js_string(_t('_bx_nexus_release_to_refresh')),
             'txt_refreshing' => bx_js_string(_t('_bx_nexus_refreshing')),
+            'main_menu' => getParam('bx_nexus_option_main_menu'),
         ));
 
         //  TODO: enable it back when it will work correctly on Android
@@ -76,6 +77,17 @@ class BxNexusModule extends BxDolModule
         return $s;
     }
 
+    public function serviceGetMenusList()
+    {
+        $a = BxDolMenuQuery::getMenuObjects();
+        array_unshift($a, array('object' => 'default', 'title' => '_sys_page_type_default', 'module' => '', 'uri' => ''));
+        $aRes = array();
+        foreach($a as $r) {
+            $aRes[] = array('key' => $r['object'], 'value' => $r['uri'] ? _t('_bx_nexus_option_main_menu_value', BxDolModule::getTitle($r['uri']), _t($r['title'])) : _t($r['title']));
+        }
+        return $aRes;
+    }
+
     protected function redirects()
     {
         $bLoggedIn = isLogged() ? 1 : 0;

+ 2 - 0
modules/boonex/nexus/install/langs/en.xml

@@ -9,6 +9,8 @@
     <string name="_bx_nexus_option_styles"><![CDATA[Custom CSS styles for mobile apps]]></string>
     <string name="_bx_nexus_option_guest_pages"><![CDATA[Comma separated list of pages URIs, which are avalilable for guests]]></string>
     <string name="_bx_nexus_option_custom_homepage"><![CDATA[Display custom homepage with login form only]]></string>
+    <string name="_bx_nexus_option_main_menu"><![CDATA[Main menu]]></string>
+    <string name="_bx_nexus_option_main_menu_value"><![CDATA[{0}: {1}]]></string>
 
 	<string name="_bx_nexus_information"><![CDATA[Information]]></string>
     <string name="_bx_nexus_information_block"><![CDATA[This module adds neccesary styles and functionalty for proper functionality of UNA Nexus apps. Installing this module isn't enough, you need to get source code of mobile apps then modify them for your needs then publish in Google and Apple stores.]]></string>

+ 2 - 1
modules/boonex/nexus/install/sql/enable.sql

@@ -13,7 +13,8 @@ SET @iCategId = LAST_INSERT_ID();
 INSERT INTO `sys_options` (`name`, `value`, `category_id`, `caption`, `type`, `extra`, `check`, `check_error`, `order`) VALUES
 ('bx_nexus_option_styles', '', @iCategId, '_bx_nexus_option_styles', 'text', '', '', '', 10),
 ('bx_nexus_option_guest_pages', 'forgot-password,create-account,terms,privacy,contact,about,home', @iCategId, '_bx_nexus_option_guest_pages', 'text', '', '', '', 20),
-('bx_nexus_option_custom_homepage', 'on', @iCategId, '_bx_nexus_option_custom_homepage', 'checkbox', '', '', '', 30);
+('bx_nexus_option_custom_homepage', 'on', @iCategId, '_bx_nexus_option_custom_homepage', 'checkbox', '', '', '', 30),
+('bx_nexus_option_main_menu', 'default', @iCategId, '_bx_nexus_option_main_menu', 'select', 'a:3:{s:6:"module";s:8:"bx_nexus";s:6:"method";s:14:"get_menus_list";s:6:"params";a:0:{}}', '', '', 40);
 
 -- Injections
 

+ 11 - 6
modules/boonex/nexus/template/js.html

@@ -1,11 +1,16 @@
 <script>
     function bx_mobile_apps_show_main_menu () {
-        if ($('.cd-dropdown-trigger').size())
-            $('.cd-dropdown-trigger').click();
-        else if ($('.cd-side-nav').size())
-            $([$('.cd-side-nav'), $('.cd-nav-trigger')]).toggleClass('nav-is-visible');
-        else
-            bx_menu_slide_inline('#bx-sliding-menu-sys_site', null, 'site');
+        if ('__main_menu__' == 'default') {
+            if ($('.cd-dropdown-trigger').size())
+                $('.cd-dropdown-trigger').click();
+            else if ($('.cd-side-nav').size())
+                $([$('.cd-side-nav'), $('.cd-nav-trigger')]).toggleClass('nav-is-visible');
+            else
+                bx_menu_slide_inline('#bx-sliding-menu-sys_site', null, 'site');
+        }
+        else {
+            bx_menu_popup('__main_menu__');
+        }
     }
 
     function bx_mobile_apps_show_profile_menu () {

+ 52 - 0
modules/boonex/russian/data/langs/bx_help_tours/ru.xml

@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources name="ru" flag="ru" title="Russian">
+    <string name="_bx_help_tours"><![CDATA[Туры помощи]]></string>
+
+	<string name="_bx_help_tours_page_tours"><![CDATA[Туры помощи]]></string>
+	<string name="_bx_help_tours_page_items"><![CDATA[Элементы помощи]]></string>
+
+	<string name="_bx_help_tours_cpt_tour_details"><![CDATA[Детали тура помощи]]></string>
+	<string name="_bx_help_tours_cpt_tour_item_details"><![CDATA[Детали эелемента помощи]]></string>
+	<string name="_bx_help_tours_cpt_items_cnt"><![CDATA[элементов помощи: {0}]]></string>
+
+	<string name="_bx_help_tours_grid_col_page"><![CDATA[Страница]]></string>
+	<string name="_bx_help_tours_grid_col_items"><![CDATA[Элементы]]></string>
+
+	<string name="_bx_help_tours_grid_col_name"><![CDATA[Имя элемента помощи]]></string>
+	<string name="_bx_help_tours_grid_col_element"><![CDATA[Блок на странице или селектор]]></string>
+	<string name="_bx_help_tours_grid_col_title"><![CDATA[Заголовок]]></string>
+
+	<string name="_bx_help_tours_grid_action_preview"><![CDATA[Предпросмотр тура]]></string>
+	<string name="_bx_help_tours_grid_action_delete"><![CDATA[Удалить тур]]></string>
+	<string name="_bx_help_tours_grid_action_add"><![CDATA[Создать тур]]></string>
+	<string name="_bx_help_tours_grid_action_edit"><![CDATA[Изменить тур]]></string>
+	<string name="_bx_help_tours_grid_action_edit_items"><![CDATA[Редактировать элемент помощи]]></string>
+
+	<string name="_bx_help_tours_grid_action_preview_item"><![CDATA[Предпросмотр элемента]]></string>
+	<string name="_bx_help_tours_grid_action_edit_item"><![CDATA[Редактировать элемент]]></string>
+	<string name="_bx_help_tours_grid_action_delete_item"><![CDATA[Удалить элемент]]></string>
+	<string name="_bx_help_tours_grid_action_add_item"><![CDATA[Добавить элемент помощи]]></string>
+
+	<string name="_bx_help_tours_form_field_page"><![CDATA[Страница]]></string>
+	<string name="_bx_help_tours_form_field_page_error"><![CDATA[Страница является обязательным полем.]]></string>
+	<string name="_bx_help_tours_form_field_overlay"><![CDATA[Темный фон]]></string>
+	<string name="_bx_help_tours_form_field_overlay_info"><![CDATA[Если включено, то во время показа тура страница будет затемнена.]]></string>
+
+	<string name="_bx_help_tours_form_field_name"><![CDATA[Имя]]></string>
+	<string name="_bx_help_tours_form_field_name_info"><![CDATA[Только для показа в студии.]]></string>
+	<string name="_bx_help_tours_form_field_name_error"><![CDATA[Имя является необходимым полем.]]></string>
+	<string name="_bx_help_tours_form_field_title"><![CDATA[Заголовок элемента помощи]]></string>
+	<string name="_bx_help_tours_form_field_text"><![CDATA[Текст элемента помощи]]></string>
+	<string name="_bx_help_tours_form_field_text_error"><![CDATA[Текст является необходимым полем]]></string>
+	<string name="_bx_help_tours_form_field_arrow"><![CDATA[Позиция стрелки элемента помощи]]></string>
+	<string name="_bx_help_tours_form_field_element"><![CDATA[Блок на странице или пользовательский элемент для показа блока помощи]]></string>
+	<string name="_bx_help_tours_form_field_element_info"><![CDATA[Если не задано, то блок помощи будет показан посередине страницы.]]></string>
+	<string name="_bx_help_tours_form_field_element_custom"><![CDATA[Пользовательский селектор]]></string>
+	<string name="_bx_help_tours_form_field_element_custom_info"><![CDATA[Для продвинутых пользователей.<br />Это мжет быть id, например "#bx-menu-toolbar-2-container".<br />Или селектор, основанный на CSS классах, например ".bx-menu-object-sys_toolbar_member"]]></string>
+
+	<string name="_bx_help_tours_arrow_option_none"><![CDATA[Нет]]></string>
+	<string name="_bx_help_tours_form_field_option_none"><![CDATA[Нет]]></string>
+	<string name="_bx_help_tours_form_field_option_custom"><![CDATA[Пользовательский]]></string>
+
+	<string name="_bx_help_tours_page_block_caption"><![CDATA[Тур Помощи]]></string>
+</resources>