diff --git a/webroot/rsrc/externals/javelin/lib/control/tokenizer/Tokenizer.js b/webroot/rsrc/externals/javelin/lib/control/tokenizer/Tokenizer.js index 60f7df8063..b626f9fc73 100644 --- a/webroot/rsrc/externals/javelin/lib/control/tokenizer/Tokenizer.js +++ b/webroot/rsrc/externals/javelin/lib/control/tokenizer/Tokenizer.js @@ -1,476 +1,480 @@ /** * @requires javelin-dom * javelin-util * javelin-stratcom * javelin-install * @provides javelin-tokenizer * @javelin */ /** * A tokenizer is a UI component similar to a text input, except that it * allows the user to input a list of items ("tokens"), generally from a fixed * set of results. A familiar example of this UI is the "To:" field of most * email clients, where the control autocompletes addresses from the user's * address book. * * @{JX.Tokenizer} is built on top of @{JX.Typeahead}, and primarily adds the * ability to choose multiple items. * * To build a @{JX.Tokenizer}, you need to do four things: * * 1. Construct it, padding a DOM node for it to attach to. See the constructor * for more information. * 2. Build a {@JX.Typeahead} and configure it with setTypeahead(). * 3. Configure any special options you want. * 4. Call start(). * * If you do this correctly, the input should suggest items and enter them as * tokens as the user types. * * When the tokenizer is focused, the CSS class `jx-tokenizer-container-focused` * is added to the container node. */ JX.install('Tokenizer', { construct : function(containerNode) { this._containerNode = containerNode; }, events : [ /** * Emitted when the value of the tokenizer changes, similar to an 'onchange' * from a . */ 'change'], properties : { limit : null, renderTokenCallback : null, browseURI: null }, members : { _containerNode : null, _root : null, _frame: null, _focus : null, _orig : null, _typeahead : null, _tokenid : 0, _tokens : null, _tokenMap : null, _initialValue : null, _seq : 0, _lastvalue : null, _placeholder : null, start : function() { if (__DEV__) { if (!this._typeahead) { throw new Error( 'JX.Tokenizer.start(): ' + 'No typeahead configured! Use setTypeahead() to provide a ' + 'typeahead.'); } } this._orig = JX.DOM.find(this._containerNode, 'input', 'tokenizer-input'); this._tokens = []; this._tokenMap = {}; try { this._frame = JX.DOM.findAbove(this._orig, 'div', 'tokenizer-frame'); } catch (e) { // Ignore, this tokenizer doesn't have a frame. } if (this._frame) { JX.DOM.alterClass(this._frame, 'has-browse', !!this.getBrowseURI()); JX.DOM.listen( this._frame, 'click', 'tokenizer-browse', JX.bind(this, this._onbrowse)); } var focus = this.buildInput(this._orig.value); this._focus = focus; var input_container = JX.DOM.scry( this._containerNode, 'div', 'tokenizer-input-container' ); input_container = input_container[0] || this._containerNode; JX.DOM.listen( focus, ['click', 'focus', 'blur', 'keydown', 'keypress', 'paste'], null, JX.bind(this, this.handleEvent)); // NOTE: Safari on the iPhone does not normally delegate click events on //
tags. This causes the event to fire. We want a click (in this // case, a touch) anywhere in the div to trigger this event so that we // can focus the input. Without this, you must tap an arbitrary area on // the left side of the input to focus it. // // http://www.quirksmode.org/blog/archives/2010/09/click_event_del.html input_container.onclick = JX.bag; JX.DOM.listen( input_container, 'click', null, JX.bind( this, function(e) { if (e.getNode('remove')) { this._remove(e.getNodeData('token').key, true); } else if (e.getTarget() == this._root) { this.focus(); } })); var root = JX.$N('div'); root.id = this._orig.id; JX.DOM.alterClass(root, 'jx-tokenizer', true); root.style.cursor = 'text'; this._root = root; root.appendChild(focus); var typeahead = this._typeahead; typeahead.setInputNode(this._focus); typeahead.start(); setTimeout(JX.bind(this, function() { var container = this._orig.parentNode; JX.DOM.setContent(container, root); var map = this._initialValue || {}; for (var k in map) { this.addToken(k, map[k]); } JX.DOM.appendContent( root, JX.$N('div', {style: {clear: 'both'}}) ); this._redraw(); }), 0); }, setInitialValue : function(map) { this._initialValue = map; return this; }, setTypeahead : function(typeahead) { typeahead.setAllowNullSelection(false); typeahead.removeListener(); typeahead.listen( 'choose', JX.bind(this, function(result) { JX.Stratcom.context().prevent(); if (this.addToken(result.rel, result.name)) { if (this.shouldHideResultsOnChoose()) { this._typeahead.hide(); } this._typeahead.clear(); this._redraw(); this.focus(); } }) ); typeahead.listen( 'query', JX.bind( this, function(query) { // TODO: We should emit a 'query' event here to allow the caller to // generate tokens on the fly, e.g. email addresses or other freeform // or algorithmic tokens. // Then do this if something handles the event. // this._focus.value = ''; // this._redraw(); // this.focus(); if (query.length) { // Prevent this event if there's any text, so that we don't submit // the form (either we created a token or we failed to create a // token; in either case we shouldn't submit). If the query is // empty, allow the event so that the form submission takes place. JX.Stratcom.context().prevent(); } })); this._typeahead = typeahead; return this; }, shouldHideResultsOnChoose : function() { return true; }, handleEvent : function(e) { this._typeahead.handleEvent(e); if (e.getPrevented()) { return; } if (e.getType() == 'click') { if (e.getTarget() == this._root) { this.focus(); e.prevent(); return; } } else if (e.getType() == 'keydown') { this._onkeydown(e); } else if (e.getType() == 'blur') { this._didblur(); // Explicitly update the placeholder since we just wiped the field // value. this._typeahead.updatePlaceholder(); } else if (e.getType() == 'focus') { this._didfocus(); } else if (e.getType() == 'paste') { setTimeout(JX.bind(this, this._redraw), 0); } }, refresh : function() { this._redraw(true); return this; }, _redraw : function(force) { // If there are tokens in the tokenizer, never show a placeholder. // Otherwise, show one if one is configured. if (JX.keys(this._tokenMap).length) { this._typeahead.setPlaceholder(null); } else { this._typeahead.setPlaceholder(this._placeholder); } var focus = this._focus; if (focus.value === this._lastvalue && !force) { return; } this._lastvalue = focus.value; var metrics = JX.DOM.textMetrics( this._focus, 'jx-tokenizer-metrics'); metrics.y = null; metrics.x += 24; metrics.setDim(focus); // NOTE: Once, long ago, we set "focus.value = focus.value;" here to fix // an issue with copy/paste in Firefox not redrawing correctly. However, // this breaks input of Japanese glyphs in Chrome, and I can't reproduce // the original issue in modern Firefox. // // If future changes muck around with things here, test that Japanese // inputs still work. Example: // // - Switch to Hiragana mode. // - Type "ni". // - This should produce a glyph, not the value "n". // // With the assignment, Chrome loses the partial input on the "n" when // the value is assigned. }, setPlaceholder : function(string) { this._placeholder = string; return this; }, addToken : function(key, value) { if (key in this._tokenMap) { return false; } var focus = this._focus; var root = this._root; var token = this.buildToken(key, value); this._tokenMap[key] = { value : value, key : key, node : token }; this._tokens.push(key); root.insertBefore(token, focus); this.invoke('change', this); return true; }, removeToken : function(key) { return this._remove(key, false); }, buildInput: function(value) { return JX.$N('input', { className: 'jx-tokenizer-input', type: 'text', autocomplete: 'off', value: value }); }, /** * Generate a token based on a key and value. The "token" and "remove" * sigils are observed by a listener in start(). */ buildToken: function(key, value) { var input = JX.$N('input', { type: 'hidden', value: key, name: this._orig.name + '[' + (this._seq++) + ']' }); var remove = JX.$N('a', { className: 'jx-tokenizer-x', sigil: 'remove' }, '\u00d7'); // U+00D7 multiplication sign var display_token = value; var attrs = { className: 'jx-tokenizer-token', sigil: 'token', meta: {key: key} }; var container = JX.$N('a', attrs); var render_callback = this.getRenderTokenCallback(); if (render_callback) { display_token = render_callback(value, key, container); } JX.DOM.setContent(container, [display_token, input, remove]); return container; }, getTokens : function() { var result = {}; for (var key in this._tokenMap) { result[key] = this._tokenMap[key].value; } return result; }, _onkeydown : function(e) { var raw = e.getRawEvent(); if (raw.ctrlKey || raw.metaKey || raw.altKey) { return; } switch (e.getSpecialKey()) { case 'tab': var completed = this._typeahead.submit(); if (!completed) { this._focus.value = ''; } break; case 'delete': if (!this._focus.value.length) { var tok; while ((tok = this._tokens.pop())) { if (this._remove(tok, true)) { break; } } } break; case 'return': // Don't subject this to token limits. break; default: if (this.getLimit() && JX.keys(this._tokenMap).length == this.getLimit()) { e.prevent(); } setTimeout(JX.bind(this, this._redraw), 0); break; } }, _remove : function(index, focus) { if (!this._tokenMap[index]) { return false; } JX.DOM.remove(this._tokenMap[index].node); delete this._tokenMap[index]; this._redraw(true); focus && this.focus(); this.invoke('change', this); return true; }, focus : function() { var focus = this._focus; JX.DOM.show(focus); // NOTE: We must fire this focus event immediately (during event // handling) for the iPhone to bring up the keyboard. Previously this // focus was wrapped in setTimeout(), but it's unclear why that was // necessary. If this is adjusted later, make sure tapping the inactive // area of the tokenizer to focus it on the iPhone still brings up the // keyboard. JX.DOM.focus(focus); }, _didfocus : function() { JX.DOM.alterClass( this._containerNode, 'jx-tokenizer-container-focused', true); }, _didblur : function() { JX.DOM.alterClass( this._containerNode, 'jx-tokenizer-container-focused', false); this._focus.value = ''; this._redraw(); }, _onbrowse: function(e) { e.kill(); var uri = this.getBrowseURI(); if (!uri) { return; } new JX.Workflow(uri, {exclude: JX.keys(this.getTokens()).join(',')}) .setHandler( JX.bind(this, function(r) { - this._typeahead.getDatasource().addResult(r.token); - this.addToken(r.key); + var source = this._typeahead.getDatasource(); + + source.addResult(r.token); + var result = source.getResult(r.key); + + this.addToken(r.key, result.name); this.focus(); })) .start(); } } });