/** * 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 = ( '