diff --git a/apps/common/main/lib/component/Button.js b/apps/common/main/lib/component/Button.js index 88f69376da..bd95feb40c 100644 --- a/apps/common/main/lib/component/Button.js +++ b/apps/common/main/lib/component/Button.js @@ -191,6 +191,10 @@ define([ 'print(\' \'); %>' + '<% } %>'; + var templateBtnCaption = + '<%= caption %>' + + ''; + var templateHugeCaption = '' + @@ -225,9 +227,7 @@ define([ '' + '' + @@ -898,7 +898,7 @@ define([ setCaption: function(caption) { if (this.caption != caption) { - if ( /icon-top/.test(this.cls) && !!this.caption && /huge/.test(this.cls) ) { + if ( /icon-top/.test(this.options.cls) && !!this.caption && /huge/.test(this.options.cls) ) { var newCaption = this.getCaptionWithBreaks(caption); this.caption = newCaption || caption; } else @@ -908,7 +908,7 @@ define([ var captionNode = this.cmpEl.find('.caption'); if (captionNode.length > 0) { - captionNode.html(this.caption); + captionNode.html((this.split || this.menu) ? _.template(templateBtnCaption)({caption: this.caption}) : this.caption); } else { this.cmpEl.find('button:first').addBack().filter('button').html(this.caption); } @@ -988,5 +988,70 @@ define([ } } }); + + Common.UI.ButtonCustom = Common.UI.Button.extend(_.extend({ + initialize : function(options) { + options.iconCls = 'icon-custom ' + (options.iconCls || ''); + Common.UI.Button.prototype.initialize.call(this, options); + + this.iconsSet = Common.UI.iconsStr2IconsObj(options.iconsSet || ['']); + var icons = Common.UI.getSuitableIcons(this.iconsSet); + this.iconNormalImg = icons['normal']; + this.iconActiveImg = icons['active']; + }, + + render: function (parentEl) { + Common.UI.Button.prototype.render.call(this, parentEl); + + var _current_active = false, + me = this; + this.cmpButtonFirst = $('button:first', this.$el || $(this.el)); + const _callback = function (records, observer) { + var _hasactive = me.cmpButtonFirst.hasClass('active') || me.cmpButtonFirst.is(':active'); + if ( _hasactive !== _current_active ) { + me.updateIcon(); + _current_active = _hasactive; + } + }; + this.cmpButtonFirst[0] && (new MutationObserver(_callback)) + .observe(this.cmpButtonFirst[0], { + attributes : true, + attributeFilter : ['class'], + }); + + if (this.menu && !this.split) { + var onMouseDown = function (e) { + _callback(); + $(document).on('mouseup', onMouseUp); + }; + var onMouseUp = function (e) { + _callback(); + $(document).off('mouseup', onMouseUp); + }; + this.cmpButtonFirst.on('mousedown', _.bind(onMouseDown, this)); + } + + this.updateIcon(); + Common.NotificationCenter.on('uitheme:changed', this.updateIcons.bind(this)); + }, + + updateIcons: function() { + var icons = Common.UI.getSuitableIcons(this.iconsSet); + this.iconNormalImg = icons['normal']; + this.iconActiveImg = icons['active']; + this.updateIcon(); + }, + + updateIcon: function() { + this.$icon && this.$icon.css({'background-image': 'url('+ (this.cmpButtonFirst && (this.cmpButtonFirst.hasClass('active') || this.cmpButtonFirst.is(':active')) ? this.iconActiveImg : this.iconNormalImg) +')'}); + }, + + applyScaling: function (ratio) { + if ( this.options.scaling !== ratio ) { + this.options.scaling = ratio; + this.updateIcons(); + } + } + }, Common.UI.ButtonCustom || {})); }); diff --git a/apps/common/main/lib/component/Mixtbar.js b/apps/common/main/lib/component/Mixtbar.js index 7bd2bbb450..4e9108f74a 100644 --- a/apps/common/main/lib/component/Mixtbar.js +++ b/apps/common/main/lib/component/Mixtbar.js @@ -392,10 +392,38 @@ define([ } }, + getTab: function(tab) { + if (tab && this.$panels) { + var panel = this.$panels.filter('[data-tab=' + tab + ']'); + return panel.length ? panel : undefined; + } + }, + + createTab: function(tab, visible) { + if (!tab.action || !tab.caption) return; + + var _panel = $(''); + this.addTab(tab, _panel, this.getLastTabIdx()); + this.setVisible(tab.action, !!visible); + return _panel; + }, + + getMorePanel: function(tab) { + return tab && btnsMore[tab] ? btnsMore[tab].panel : null; + }, + + getLastTabIdx: function() { + return config.tabs.length; + }, + isCompact: function () { return this.isFolded; }, + isExpanded: function () { + return !this.isFolded || optsFold.$bar && optsFold.$bar.hasClass('expanded'); + }, + hasTabInvisible: function() { if ($boxTabs.length<1) return false; @@ -447,6 +475,9 @@ define([ _.each($active.find('.btn-slot .x-huge'), function(item) { _btns.push($(item).closest('.btn-slot')); }); + btnsMore[data.tab] && btnsMore[data.tab].panel && _.each(btnsMore[data.tab].panel.find('.btn-slot .x-huge'), function(item) { + _btns.push($(item).closest('.btn-slot')); + }); data.buttons = _btns; } if (!_flex) { @@ -603,6 +634,83 @@ define([ } }, + addCustomItems: function(tab, added, removed) { + if (!tab.action) return; + + var $panel = tab.action ? this.getTab(tab.action) || this.createTab(tab, true) || this.getTab('plugins') : null, + $morepanel = this.getMorePanel(tab.action), + $moresection = $panel ? $panel.find('.more-box') : null, + compactcls = ''; + ($moresection.length<1) && ($moresection = null); + if ($panel) { + if (removed) { + removed.forEach(function(button, index) { + if (button.cmpEl) { + var group = button.cmpEl.closest('.group'); + button.cmpEl.closest('.btn-slot').remove(); + if (group.children().length<1) { + var in_more = group.closest('.more-container').length>0; + in_more ? group.next('.separator').remove() : group.prev('.separator').remove(); + group.remove(); + if (in_more && $morepanel.children().filter('.group').length === 0) { + btnsMore[tab.action] && btnsMore[tab.action].isActive() && btnsMore[tab.action].toggle(false); + $moresection && $moresection.css('display', "none"); + } + } + } + }); + $panel.find('.btn-slot:not(.slot-btn-more).x-huge').last().hasClass('compactwidth') && (compactcls = 'compactwidth'); + } + added && added.forEach(function(button, index) { + var _groups, _group; + if ($morepanel) { + _groups = $morepanel.children().filter('.group'); + if (_groups.length>0) { + $moresection = null; + $panel = $morepanel; + compactcls = 'compactwidth'; + } + } + if (!_groups || _groups.length<1) + _groups = $panel.children().filter('.group'); + + if (_groups.length>0 && !button.options.separator && index>0) // add first button to new group + _group = $(_groups[_groups.length-1]); + else { + if (button.options.separator) { + var el = $('
'); + $moresection ? $moresection.before(el) : el.appendTo($panel); + } + _group = $(''); + $moresection ? $moresection.before(_group) : _group.appendTo($panel); + } + var $slot = $('').appendTo(_group); + button.render($slot); + }); + } + this.clearActiveData(tab.action); + this.processPanelVisible(null, true); + + var visible = !this.isTabEmpty(tab.action) && Common.UI.LayoutManager.isElementVisible('toolbar-' + tab.action); + this.setVisible(tab.action, visible); + if (!visible && this.isTabActive(tab.action) && this.isExpanded()) { + if (this.getTab('home')) + this.setTab('home'); + else { + tab = this.$tabs.siblings(':not(.x-lone):visible').first().find('> a[data-tab]').data('tab'); + this.setTab(tab); + } + } + }, + + isTabEmpty: function(tab) { + var $panel = this.getTab(tab), + $morepanel = this.getMorePanel(tab), + $moresection = $panel ? $panel.find('.more-box') : null; + ($moresection.length<1) && ($moresection = null); + return $panel ? !($panel.find('> .group').length>0 || $morepanel && $morepanel.find('.group').length>0) : false; + }, + resizeToolbar: function(reset) { var $active = this.$panels.filter('.active'), more_section = $active.find('.more-box'); diff --git a/apps/common/main/lib/controller/LayoutManager.js b/apps/common/main/lib/controller/LayoutManager.js index b0f9f4dd6f..5bff58ac07 100644 --- a/apps/common/main/lib/controller/LayoutManager.js +++ b/apps/common/main/lib/controller/LayoutManager.js @@ -47,10 +47,13 @@ if (Common.UI === undefined) { Common.UI.LayoutManager = new(function() { var _config, - _licensed; - var _init = function(config, licensed) { + _licensed, + _api, + _lastInternalTabIdx = 10; + var _init = function(config, licensed, api) { _config = config; _licensed = licensed; + _api = api; }; var _applyCustomization = function(config, el, prefix) { @@ -110,11 +113,193 @@ Common.UI.LayoutManager = new(function() { } }; + var _findCustomButton = function(toolbar, action, guid, id) { + if (toolbar && toolbar.customButtonsArr && toolbar.customButtonsArr[guid] ) { + for (var i=0; i< toolbar.customButtonsArr[guid].length; i++) { + var btn = toolbar.customButtonsArr[guid][i]; + if (btn.options.tabid === action && btn.options.guid === guid && btn.options.value === id) { + return btn; + } + } + } + } + + var _findRemovedButtons = function(toolbar, action, guid, items) { + var arr = []; + if (toolbar && toolbar.customButtonsArr && toolbar.customButtonsArr[guid] ) { + if (!items || items.length<1) { + arr = toolbar.customButtonsArr[guid]; + toolbar.customButtonsArr[guid] = undefined; + } else { + for (var i=0; i< toolbar.customButtonsArr[guid].length; i++) { + var btn = toolbar.customButtonsArr[guid][i]; + if (btn.options.tabid === action && !_.findWhere(items, {id: btn.options.value})) { + arr.push(btn); + toolbar.customButtonsArr[guid].splice(i, 1); + i--; + } + } + } + } + return arr; + } + + var _fillButtonMenu = function(items, guid, lang, toMenu) { + if (toMenu) + toMenu.removeAll(); + else { + toMenu = new Common.UI.Menu({ + cls: 'shifted-right', + menuAlign: 'tl-tr', + items: [] + }); + toMenu.on('item:click', function(menu, mi, e) { + _api && _api.onPluginToolbarMenuItemClick(mi.options.guid, mi.value); + }); + } + items.forEach(function(menuItem) { + if (menuItem.separator) toMenu.addItem({caption: '--'}); + menuItem.text && toMenu.addItem({ + caption: menuItem.text || '', + value: menuItem.id, + menu: menuItem.items ? _fillButtonMenu(menuItem.items, guid, lang) : false, + iconImg: Common.UI.getSuitableIcons(Common.UI.iconsStr2IconsObj(menuItem.icons)), + guid: guid + }); + }); + return toMenu; + } + + var _addCustomItems = function (toolbar, data) { + if (!data) return; + + var lang = Common.Locale.getCurrentLanguage(), + btns = []; + data.forEach(function(plugin) { + /* + plugin = { + guid: 'plugin-guid', + tabs: [ + { + id: 'tab-id', + text: 'caption', + items: [ + { + id: 'button-id', + type: 'button'='big-button' or 'small-button', + icons: 'template string' or object + text: 'caption' or - can be empty + hint: 'hint', + separator: true/false - inserted before item, + split: true/false - used when has menu + items: [ + { + id: 'item-id', + text: 'caption' + separator: true/false - inserted before item, + icons: 'template string' or object + } + ], + enableToggle: true/false - can press and depress button, only when no menu or has split menu + lockInViewMode: true/false - lock in view modes (preview review, view forms, disconnect, etc.), + disabled: true/false + } + ] + }, + { + id: 'tab-id', + text: 'caption', + items: [...] + }, + ] + } + */ + plugin.tabs && plugin.tabs.forEach(function(tab) { + if (tab) { + var added = [], + removed = _findRemovedButtons(toolbar, tab.id, plugin.guid, tab.items); + tab.items && tab.items.forEach(function(item, index) { + var btn = _findCustomButton(toolbar, tab.id, plugin.guid, item.id), + _set = Common.enumLock; + if (btn) { // change caption, hint, disable state, menu items + if (btn instanceof Common.UI.Button) { + var caption = item.text || ''; + if (btn.options.caption !== (caption || ' ')) { + btn.cmpEl.closest('.btn-slot.x-huge').toggleClass('nocaption', !caption); + btn.setCaption(caption || ' '); + btn.options.caption = caption || ' '; + } + btn.updateHint(item.hint || ''); + (item.disabled!==undefined) && Common.Utils.lockControls(_set.customLock, !!item.disabled, {array: [btn]}); + if (btn.menu && item.items && item.items.length > 0) {// update menu items + if (typeof btn.menu !== 'object') { + btn.setMenu(new Common.UI.Menu({items: []})); + btn.menu.on('item:click', function(menu, mi, e) { + _api && _api.onPluginToolbarMenuItemClick(mi.options.guid, mi.value); + }); + } + _fillButtonMenu(item.items, plugin.guid, lang, btn.menu); + } + } + return; + } + + if (item.type==='button' || item.type==='big-button') { + btn = new Common.UI.ButtonCustom({ + cls: 'btn-toolbar x-huge icon-top', + iconsSet: item.icons, + caption: item.text || ' ', + menu: item.items, + split: item.items && !!item.split, + enableToggle: item.enableToggle && (!item.items || !!item.split), + value: item.id, + guid: plugin.guid, + tabid: tab.id, + separator: item.separator, + hint: item.hint || '', + lock: item.lockInViewMode ? [_set.customLock, _set.viewMode, _set.previewReviewMode, _set.viewFormMode, _set.docLockView, _set.docLockForms, _set.docLockComments, _set.selRangeEdit, _set.editFormula ] : [_set.customLock], + dataHint: '1', + dataHintDirection: 'bottom', + dataHintOffset: 'small' + }); + + if (item.items && typeof item.items === 'object') { + btn.setMenu(new Common.UI.Menu({items: []})); + btn.menu.on('item:click', function(menu, mi, e) { + _api && _api.onPluginToolbarMenuItemClick(mi.options.guid, mi.value); + }); + _fillButtonMenu(item.items, plugin.guid, lang, btn.menu); + } + if ( !btn.menu || btn.split) { + btn.on('click', function(b, e) { + _api && _api.onPluginToolbarMenuItemClick(b.options.guid, b.options.value, b.pressed); + }); + } + added.push(btn); + item.disabled && Common.Utils.lockControls(_set.customLock, item.disabled, {array: [btn]}); + } + }); + + toolbar.addCustomItems({action: tab.id, caption: tab.text || ''}, added, removed); + if (!toolbar.customButtonsArr) + toolbar.customButtonsArr = []; + if (!toolbar.customButtonsArr[plugin.guid]) + toolbar.customButtonsArr[plugin.guid] = []; + Array.prototype.push.apply(toolbar.customButtonsArr[plugin.guid], added); + Array.prototype.push.apply(btns, added); + } + }); + }); + return btns; + }; + return { init: _init, applyCustomization: _applyCustomization, isElementVisible: _isElementVisible, - getInitValue: _getInitValue + getInitValue: _getInitValue, + lastTabIdx: _lastInternalTabIdx, + addCustomItems: _addCustomItems } })(); diff --git a/apps/common/main/lib/controller/Plugins.js b/apps/common/main/lib/controller/Plugins.js index e0702a7311..fbae06159b 100644 --- a/apps/common/main/lib/controller/Plugins.js +++ b/apps/common/main/lib/controller/Plugins.js @@ -68,7 +68,7 @@ define([ var tab = {action: 'plugins', caption: me.viewPlugins.groupCaption, dataHintTitle: 'E', layoutname: 'toolbar-plugins'}; me.$toolbarPanelPlugins = me.viewPlugins.getPanel(); me.toolbar = toolbar; - toolbar.addTab(tab, me.$toolbarPanelPlugins, 10); // TODO: clear plugins list in left panel + toolbar.addTab(tab, me.$toolbarPanelPlugins, Common.UI.LayoutManager.lastTabIdx); // TODO: clear plugins list in left panel } } }, @@ -421,6 +421,11 @@ define([ me.backgroundPlugins.push(model); return; } + if (model.get('tab')) { + me.toolbar && me.toolbar.addCustomItems(model.get('tab'), [me.viewPlugins.createPluginButton(model)]); + return; + } + //if (new_rank === 1 || new_rank === 2) return; // for test if ((new_rank === 0 || new_rank === 2) && !isBackground) { _group = me.addBackgroundPluginsButton(_group); @@ -883,7 +888,8 @@ define([ original: item, isDisplayedInViewer: isDisplayedInViewer, isBackgroundPlugin: pluginVisible && isBackgroundPlugin, - isSystem: isSystem + isSystem: isSystem, + tab: item.tab ? {action: item.tab.id, caption: ((typeof item.tab.text == 'object') ? item.tab.text[lang] || item.tab.text['en'] : item.tab.text) || ''} : undefined }; updatedItem ? updatedItem.set(props) : arr.push(new Common.Models.Plugin(props)); } diff --git a/apps/common/main/lib/util/utils.js b/apps/common/main/lib/util/utils.js index fa220d885a..7967866672 100644 --- a/apps/common/main/lib/util/utils.js +++ b/apps/common/main/lib/util/utils.js @@ -1210,3 +1210,162 @@ Common.UI.isRTL = function () { return window.isrtl; }; + +Common.UI.iconsStr2IconsObj = function(icons) { + let result = icons; + if (typeof result === 'string' && result.indexOf('%') !== -1) { + /* + valid params: + theme-type - {string} theme type (light|dark|common) + theme-name - {string} the name of theme + state - {string} state of icons for different situations (normal|hover|active) + scale - {string} list of avaliable scales (100|125|150|175|200|default|extended) + extension - {string} use it after symbol "." (png|jpeg|svg) + + Example: "resources/%theme-type%(light|dark)/%state%(normal)icon%scale%(default).%extension%(png)" + */ + let scaleValue = { + '100%' : '.', + '125%' : '@1.25x.', + '150%' : '@1.5x.', + '175%' : '@1.75x.', + '200%' : '@2x.' + } + let arrParams = ['theme-type', 'theme-name' ,'state', 'scale', 'extension'], + start = result.indexOf('%'), + template = result.substring(start).replace(/[/.]/g, ('')), + commonPart = result.substring(0, start), + end = 0, + param = null, + values = null, + iconName = '', + tempObj = {}; + + result = []; + + for (let index = 0; index < arrParams.length; index++) { + param = arrParams[index]; + start = template.indexOf(param) - 1; + if (start < 0 ) + continue; + + end = param.length + 2; + template = template.substring(0, start) + template.substring(start + end); + start = template.indexOf('(', 0); + end = template.indexOf(')', 0); + values = template.substring((start + 1), end); + template = template.substring(0, start) + template.substring(++end); + tempObj[param] = values.split('|'); + } + + if (template.length) { + iconName = template; + } else { + let arr = commonPart.split('/'); + iconName = arr.pop().replace(/\./g, ''); + commonPart = arr.join('/') + '/'; + } + + // we don't work with svg yet. Change it when we will work with it (extended variant). + if (tempObj['scale'] && (tempObj['scale'] == 'default' || tempObj['scale'] == 'extended') ) { + tempObj['scale'] = ['100', '125', '150', '175', '200']; + } else if (!tempObj['scale']) { + tempObj['scale'] = ['100']; + } + + if (!tempObj['state']) { + tempObj['state'] = ['normal']; + } + + if (!iconName) { + iconName = 'icon'; + } + + let bHasName = !!tempObj['theme-name']; + let bHasType = (tempObj['theme-type'] && tempObj['theme-type'][0] !== 'common'); + let arrThemes = bHasName ? tempObj['theme-name'] : (bHasType ? tempObj['theme-type'] : []); + let paramName = bHasName ? 'theme' : 'style'; + if (arrThemes.length) { + for (let thInd = 0; thInd < arrThemes.length; thInd++) { + let obj = {}; + obj[paramName] = arrThemes[thInd]; + result.push(obj); + } + } else { + result.push({}); + } + + for (let index = 0; index < result.length; index++) { + for (let scaleInd = 0; scaleInd < tempObj['scale'].length; scaleInd++) { + let themePath = (result[index][paramName] || 'img') + '/'; + let scale = tempObj['scale'][scaleInd] + '%'; + let obj = {}; + for (let stateInd = 0; stateInd < tempObj['state'].length; stateInd++) { + let state = tempObj['state'][stateInd]; + obj[state] = commonPart + themePath + (state == 'normal' ? '' : (state + '_')) + iconName + (scaleValue[scale] || '.') + tempObj['extension'][0]; + } + result[index][scale] = obj; + } + } + } + return result; +} + +Common.UI.getSuitableIcons = function(icons) { + if (!icons) return; + + icons = Common.UI.iconsStr2IconsObj(icons); + if (icons.length && typeof icons[0] !== 'string') { + var theme = Common.UI.Themes.currentThemeId().toLowerCase(), + style = Common.UI.Themes.isDarkTheme() ? 'dark' : 'light', + idx = -1; + for (var i=0; i