/** * ComboBox.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ import DomQuery from 'tinymce/core/api/dom/DomQuery'; import Factory from 'tinymce/core/api/ui/Factory'; import Tools from 'tinymce/core/api/util/Tools'; import VK from 'tinymce/core/api/util/VK'; import DomUtils from './DomUtils'; import Widget from './Widget'; /** * This class creates a combobox control. Select box that you select a value from or * type a value into. * * @-x-less ComboBox.less * @class tinymce.ui.ComboBox * @extends tinymce.ui.Widget */ export default Widget.extend({ /** * Constructs a new control instance with the specified settings. * * @constructor * @param {Object} settings Name/value object with settings. * @setting {String} placeholder Placeholder text to display. */ init (settings) { const self = this; self._super(settings); settings = self.settings; self.classes.add('combobox'); self.subinput = true; self.ariaTarget = 'inp'; // TODO: Figure out a better way settings.menu = settings.menu || settings.values; if (settings.menu) { settings.icon = 'caret'; } self.on('click', function (e) { let elm = e.target; const root = self.getEl(); if (!DomQuery.contains(root, elm) && elm !== root) { return; } while (elm && elm !== root) { if (elm.id && elm.id.indexOf('-open') !== -1) { self.fire('action'); if (settings.menu) { self.showMenu(); if (e.aria) { self.menu.items()[0].focus(); } } } elm = elm.parentNode; } }); // TODO: Rework this self.on('keydown', function (e) { let rootControl; if (e.keyCode === 13 && e.target.nodeName === 'INPUT') { e.preventDefault(); // Find root control that we can do toJSON on self.parents().reverse().each(function (ctrl) { if (ctrl.toJSON) { rootControl = ctrl; return false; } }); // Fire event on current text box with the serialized data of the whole form self.fire('submit', { data: rootControl.toJSON() }); } }); self.on('keyup', function (e) { if (e.target.nodeName === 'INPUT') { const oldValue = self.state.get('value'); const newValue = e.target.value; if (newValue !== oldValue) { self.state.set('value', newValue); self.fire('autocomplete', e); } } }); self.on('mouseover', function (e) { const tooltip = self.tooltip().moveTo(-0xFFFF); if (self.statusLevel() && e.target.className.indexOf(self.classPrefix + 'status') !== -1) { const statusMessage = self.statusMessage() || 'Ok'; const rel = tooltip.text(statusMessage).show().testMoveRel(e.target, ['bc-tc', 'bc-tl', 'bc-tr']); tooltip.classes.toggle('tooltip-n', rel === 'bc-tc'); tooltip.classes.toggle('tooltip-nw', rel === 'bc-tl'); tooltip.classes.toggle('tooltip-ne', rel === 'bc-tr'); tooltip.moveRel(e.target, rel); } }); }, statusLevel (value) { if (arguments.length > 0) { this.state.set('statusLevel', value); } return this.state.get('statusLevel'); }, statusMessage (value) { if (arguments.length > 0) { this.state.set('statusMessage', value); } return this.state.get('statusMessage'); }, showMenu () { const self = this; const settings = self.settings; let menu; if (!self.menu) { menu = settings.menu || []; // Is menu array then auto constuct menu control if (menu.length) { menu = { type: 'menu', items: menu }; } else { menu.type = menu.type || 'menu'; } self.menu = Factory.create(menu).parent(self).renderTo(self.getContainerElm()); self.fire('createmenu'); self.menu.reflow(); self.menu.on('cancel', function (e) { if (e.control === self.menu) { self.focus(); } }); self.menu.on('show hide', function (e) { e.control.items().each(function (ctrl) { ctrl.active(ctrl.value() === self.value()); }); }).fire('show'); self.menu.on('select', function (e) { self.value(e.control.value()); }); self.on('focusin', function (e) { if (e.target.tagName.toUpperCase() === 'INPUT') { self.menu.hide(); } }); self.aria('expanded', true); } self.menu.show(); self.menu.layoutRect({ w: self.layoutRect().w }); self.menu.moveRel(self.getEl(), self.isRtl() ? ['br-tr', 'tr-br'] : ['bl-tl', 'tl-bl']); }, /** * Focuses the input area of the control. * * @method focus */ focus () { this.getEl('inp').focus(); }, /** * Repaints the control after a layout operation. * * @method repaint */ repaint () { const self = this, elm = self.getEl(), openElm = self.getEl('open'), rect = self.layoutRect(); let width, lineHeight, innerPadding = 0; const inputElm = elm.firstChild; if (self.statusLevel() && self.statusLevel() !== 'none') { innerPadding = ( parseInt(DomUtils.getRuntimeStyle(inputElm, 'padding-right'), 10) - parseInt(DomUtils.getRuntimeStyle(inputElm, 'padding-left'), 10) ); } if (openElm) { width = rect.w - DomUtils.getSize(openElm).width - 10; } else { width = rect.w - 10; } // Detect old IE 7+8 add lineHeight to align caret vertically in the middle const doc: any = document; if (doc.all && (!doc.documentMode || doc.documentMode <= 8)) { lineHeight = (self.layoutRect().h - 2) + 'px'; } DomQuery(inputElm).css({ width: width - innerPadding, lineHeight }); self._super(); return self; }, /** * Post render method. Called after the control has been rendered to the target. * * @method postRender * @return {tinymce.ui.ComboBox} Current combobox instance. */ postRender () { const self = this; DomQuery(this.getEl('inp')).on('change', function (e) { self.state.set('value', e.target.value); self.fire('change', e); }); return self._super(); }, /** * Renders the control as a HTML string. * * @method renderHtml * @return {String} HTML representing the control. */ renderHtml () { const self = this, id = self._id, settings = self.settings, prefix = self.classPrefix; const value = self.state.get('value') || ''; let icon, text, openBtnHtml = '', extraAttrs = '', statusHtml = ''; if ('spellcheck' in settings) { extraAttrs += ' spellcheck="' + settings.spellcheck + '"'; } if (settings.maxLength) { extraAttrs += ' maxlength="' + settings.maxLength + '"'; } if (settings.size) { extraAttrs += ' size="' + settings.size + '"'; } if (settings.subtype) { extraAttrs += ' type="' + settings.subtype + '"'; } statusHtml = ''; if (self.disabled()) { extraAttrs += ' disabled="disabled"'; } icon = settings.icon; if (icon && icon !== 'caret') { icon = prefix + 'ico ' + prefix + 'i-' + settings.icon; } text = self.state.get('text'); if (icon || text) { openBtnHtml = ( '
' + '' + '
' ); self.classes.add('has-open'); } return ( '
' + '' + statusHtml + openBtnHtml + '
' ); }, value (value) { if (arguments.length) { this.state.set('value', value); return this; } // Make sure the real state is in sync if (this.state.get('rendered')) { this.state.set('value', this.getEl('inp').value); } return this.state.get('value'); }, showAutoComplete (items, term) { const self = this; if (items.length === 0) { self.hideMenu(); return; } const insert = function (value, title) { return function () { self.fire('selectitem', { title, value }); }; }; if (self.menu) { self.menu.items().remove(); } else { self.menu = Factory.create({ type: 'menu', classes: 'combobox-menu', layout: 'flow' }).parent(self).renderTo(); } Tools.each(items, function (item) { self.menu.add({ text: item.title, url: item.previewUrl, match: term, classes: 'menu-item-ellipsis', onclick: insert(item.value, item.title) }); }); self.menu.renderNew(); self.hideMenu(); self.menu.on('cancel', function (e) { if (e.control.parent() === self.menu) { e.stopPropagation(); self.focus(); self.hideMenu(); } }); self.menu.on('select', function () { self.focus(); }); const maxW = self.layoutRect().w; self.menu.layoutRect({ w: maxW, minW: 0, maxW }); self.menu.repaint(); self.menu.reflow(); self.menu.show(); self.menu.moveRel(self.getEl(), self.isRtl() ? ['br-tr', 'tr-br'] : ['bl-tl', 'tl-bl']); }, hideMenu () { if (this.menu) { this.menu.hide(); } }, bindStates () { const self = this; self.state.on('change:value', function (e) { if (self.getEl('inp').value !== e.value) { self.getEl('inp').value = e.value; } }); self.state.on('change:disabled', function (e) { self.getEl('inp').disabled = e.value; }); self.state.on('change:statusLevel', function (e) { const statusIconElm = self.getEl('status'); const prefix = self.classPrefix, value = e.value; DomUtils.css(statusIconElm, 'display', value === 'none' ? 'none' : ''); DomUtils.toggleClass(statusIconElm, prefix + 'i-checkmark', value === 'ok'); DomUtils.toggleClass(statusIconElm, prefix + 'i-warning', value === 'warn'); DomUtils.toggleClass(statusIconElm, prefix + 'i-error', value === 'error'); self.classes.toggle('has-status', value !== 'none'); self.repaint(); }); DomUtils.on(self.getEl('status'), 'mouseleave', function () { self.tooltip().hide(); }); self.on('cancel', function (e) { if (self.menu && self.menu.visible()) { e.stopPropagation(); self.hideMenu(); } }); const focusIdx = function (idx, menu) { if (menu && menu.items().length > 0) { menu.items().eq(idx)[0].focus(); } }; self.on('keydown', function (e) { const keyCode = e.keyCode; if (e.target.nodeName === 'INPUT') { if (keyCode === VK.DOWN) { e.preventDefault(); self.fire('autocomplete'); focusIdx(0, self.menu); } else if (keyCode === VK.UP) { e.preventDefault(); focusIdx(-1, self.menu); } } }); return self._super(); }, remove () { DomQuery(this.getEl('inp')).off(); if (this.menu) { this.menu.remove(); } this._super(); } });