/* ***** BEGIN LICENSE BLOCK ***** * Distributed under the BSD license: * * Copyright (c) 2010, Ajax.org B.V. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Ajax.org B.V. nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * ***** END LICENSE BLOCK ***** */ define(function(require, exports, module) { "use strict"; var event = require("../lib/event"); var useragent = require("../lib/useragent"); var dom = require("../lib/dom"); var lang = require("../lib/lang"); var BROKEN_SETDATA = useragent.isChrome < 18; var TextInput = function(parentNode, host) { var text = dom.createElement("textarea"); text.className = "ace_text-input"; if (useragent.isTouchPad) text.setAttribute("x-palm-disable-auto-cap", true); text.wrap = "off"; text.autocorrect = "off"; text.autocapitalize = "off"; text.spellcheck = false; text.style.opacity = "0"; parentNode.insertBefore(text, parentNode.firstChild); var PLACEHOLDER = "\x01\x01"; var cut = false; var copied = false; var pasted = false; var inComposition = false; var tempStyle = ''; var isSelectionEmpty = true; // FOCUS // ie9 throws error if document.activeElement is accessed too soon try { var isFocused = document.activeElement === text; } catch(e) {} event.addListener(text, "blur", function() { host.onBlur(); isFocused = false; }); event.addListener(text, "focus", function() { isFocused = true; host.onFocus(); resetSelection(); }); this.focus = function() { text.focus(); }; this.blur = function() { text.blur(); }; this.isFocused = function() { return isFocused; }; // modifying selection of blured textarea can focus it (chrome mac/linux) var syncSelection = lang.delayedCall(function() { isFocused && resetSelection(isSelectionEmpty); }); var syncValue = lang.delayedCall(function() { if (!inComposition) { text.value = PLACEHOLDER; isFocused && resetSelection(); } }); function resetSelection(isEmpty) { if (inComposition) return; if (inputHandler) { selectionStart = 0; selectionEnd = isEmpty ? 0 : text.value.length - 1; } else { var selectionStart = isEmpty ? 2 : 1; var selectionEnd = 2; } // on firefox this throws if textarea is hidden try { text.setSelectionRange(selectionStart, selectionEnd); } catch(e){} } function resetValue() { if (inComposition) return; text.value = PLACEHOLDER; //http://code.google.com/p/chromium/issues/detail?id=76516 if (useragent.isWebKit) syncValue.schedule(); } useragent.isWebKit || host.addEventListener('changeSelection', function() { if (host.selection.isEmpty() != isSelectionEmpty) { isSelectionEmpty = !isSelectionEmpty; syncSelection.schedule(); } }); resetValue(); if (isFocused) host.onFocus(); var isAllSelected = function(text) { return text.selectionStart === 0 && text.selectionEnd === text.value.length; }; // IE8 does not support setSelectionRange if (!text.setSelectionRange && text.createTextRange) { text.setSelectionRange = function(selectionStart, selectionEnd) { var range = this.createTextRange(); range.collapse(true); range.moveStart('character', selectionStart); range.moveEnd('character', selectionEnd); range.select(); }; isAllSelected = function(text) { try { var range = text.ownerDocument.selection.createRange(); }catch(e) {} if (!range || range.parentElement() != text) return false; return range.text == text.value; } } if (useragent.isOldIE) { var inPropertyChange = false; var onPropertyChange = function(e){ if (inPropertyChange) return; var data = text.value; if (inComposition || !data || data == PLACEHOLDER) return; // can happen either after delete or during insert operation if (e && data == PLACEHOLDER[0]) return syncProperty.schedule(); sendText(data); // ie8 calls propertychange handlers synchronously! inPropertyChange = true; resetValue(); inPropertyChange = false; }; var syncProperty = lang.delayedCall(onPropertyChange); event.addListener(text, "propertychange", onPropertyChange); var keytable = { 13:1, 27:1 }; event.addListener(text, "keyup", function (e) { if (inComposition && (!text.value || keytable[e.keyCode])) setTimeout(onCompositionEnd, 0); if ((text.value.charCodeAt(0)||0) < 129) { return syncProperty.call(); } inComposition ? onCompositionUpdate() : onCompositionStart(); }); // when user presses backspace after focusing the editor // propertychange isn't called for the next character event.addListener(text, "keydown", function (e) { syncProperty.schedule(50); }); } var onSelect = function(e) { if (cut) { cut = false; } else if (copied) { copied = false; } else if (isAllSelected(text)) { host.selectAll(); resetSelection(); } else if (inputHandler) { resetSelection(host.selection.isEmpty()); } }; var inputHandler = null; this.setInputHandler = function(cb) {inputHandler = cb}; this.getInputHandler = function() {return inputHandler}; var afterContextMenu = false; var sendText = function(data) { if (inputHandler) { data = inputHandler(data); inputHandler = null; } if (pasted) { resetSelection(); if (data) host.onPaste(data); pasted = false; } else if (data == PLACEHOLDER.charAt(0)) { if (afterContextMenu) host.execCommand("del", {source: "ace"}); else // some versions of android do not fire keydown when pressing backspace host.execCommand("backspace", {source: "ace"}); } else { if (data.substring(0, 2) == PLACEHOLDER) data = data.substr(2); else if (data.charAt(0) == PLACEHOLDER.charAt(0)) data = data.substr(1); else if (data.charAt(data.length - 1) == PLACEHOLDER.charAt(0)) data = data.slice(0, -1); // can happen if undo in textarea isn't stopped if (data.charAt(data.length - 1) == PLACEHOLDER.charAt(0)) data = data.slice(0, -1); if (data) host.onTextInput(data); } if (afterContextMenu) afterContextMenu = false; }; var onInput = function(e) { // console.log("onInput", inComposition) if (inComposition) return; var data = text.value; sendText(data); resetValue(); }; var onCut = function(e) { var data = host.getCopyText(); if (!data) { event.preventDefault(e); return; } var clipboardData = e.clipboardData || window.clipboardData; if (clipboardData && !BROKEN_SETDATA) { // Safari 5 has clipboardData object, but does not handle setData() var supported = clipboardData.setData("Text", data); if (supported) { host.onCut(); event.preventDefault(e); } } if (!supported) { cut = true; text.value = data; text.select(); setTimeout(function(){ cut = false; resetValue(); resetSelection(); host.onCut(); }); } }; var onCopy = function(e) { var data = host.getCopyText(); if (!data) { event.preventDefault(e); return; } var clipboardData = e.clipboardData || window.clipboardData; if (clipboardData && !BROKEN_SETDATA) { // Safari 5 has clipboardData object, but does not handle setData() var supported = clipboardData.setData("Text", data); if (supported) { host.onCopy(); event.preventDefault(e); } } if (!supported) { copied = true; text.value = data; text.select(); setTimeout(function(){ copied = false; resetValue(); resetSelection(); host.onCopy(); }); } }; var onPaste = function(e) { var clipboardData = e.clipboardData || window.clipboardData; if (clipboardData) { var data = clipboardData.getData("Text"); if (data) host.onPaste(data); if (useragent.isIE) setTimeout(resetSelection); event.preventDefault(e); } else { text.value = ""; pasted = true; } }; event.addCommandKeyListener(text, host.onCommandKey.bind(host)); event.addListener(text, "select", onSelect); event.addListener(text, "input", onInput); event.addListener(text, "cut", onCut); event.addListener(text, "copy", onCopy); event.addListener(text, "paste", onPaste); // Opera has no clipboard events if (!('oncut' in text) || !('oncopy' in text) || !('onpaste' in text)){ event.addListener(parentNode, "keydown", function(e) { if ((useragent.isMac && !e.metaKey) || !e.ctrlKey) return; switch (e.keyCode) { case 67: onCopy(e); break; case 86: onPaste(e); break; case 88: onCut(e); break; } }); } // COMPOSITION var onCompositionStart = function(e) { if (inComposition) return; // console.log("onCompositionStart", inComposition) inComposition = {}; host.onCompositionStart(); setTimeout(onCompositionUpdate, 0); host.on("mousedown", onCompositionEnd); if (!host.selection.isEmpty()) { host.insert(""); host.session.markUndoGroup(); host.selection.clearSelection(); } host.session.markUndoGroup(); }; var onCompositionUpdate = function() { // console.log("onCompositionUpdate", inComposition && JSON.stringify(text.value)) if (!inComposition) return; var val = text.value.replace(/\x01/g, ""); if (inComposition.lastValue === val) return; host.onCompositionUpdate(val); if (inComposition.lastValue) host.undo(); inComposition.lastValue = val; if (inComposition.lastValue) { var r = host.selection.getRange(); host.insert(inComposition.lastValue); host.session.markUndoGroup(); inComposition.range = host.selection.getRange(); host.selection.setRange(r); host.selection.clearSelection(); } }; var onCompositionEnd = function(e) { // console.log("onCompositionEnd", inComposition &&inComposition.lastValue) var c = inComposition; inComposition = false; var timer = setTimeout(function() { timer = null; var str = text.value.replace(/\x01/g, ""); // console.log(str, c.lastValue) if (inComposition) return else if (str == c.lastValue) resetValue(); else if (!c.lastValue && str) { resetValue(); sendText(str); } }); inputHandler = function compositionInputHandler(str) { // console.log("onCompositionEnd", str, c.lastValue) if (timer) clearTimeout(timer); str = str.replace(/\x01/g, ""); if (str == c.lastValue) return ""; if (c.lastValue && timer) host.undo(); return str; }; host.onCompositionEnd(); host.removeListener("mousedown", onCompositionEnd); if (e.type == "compositionend" && c.range) { host.selection.setRange(c.range); } }; var syncComposition = lang.delayedCall(onCompositionUpdate, 50); event.addListener(text, "compositionstart", onCompositionStart); if (useragent.isGecko) { event.addListener(text, "text", function(){syncComposition.schedule()}); } else { event.addListener(text, "keyup", function(){syncComposition.schedule()}); event.addListener(text, "keydown", function(){syncComposition.schedule()}); } event.addListener(text, "compositionend", onCompositionEnd); this.getElement = function() { return text; }; this.setReadOnly = function(readOnly) { text.readOnly = readOnly; }; this.onContextMenu = function(e) { afterContextMenu = true; if (!tempStyle) tempStyle = text.style.cssText; text.style.cssText = "z-index:100000;" + (useragent.isIE ? "opacity:0.1;" : ""); resetSelection(host.selection.isEmpty()); host._emit("nativecontextmenu", {target: host, domEvent: e}); var rect = host.container.getBoundingClientRect(); var style = dom.computedStyle(host.container); var top = rect.top + (parseInt(style.borderTopWidth) || 0); var left = rect.left + (parseInt(rect.borderLeftWidth) || 0); var maxTop = rect.bottom - top - text.clientHeight; var move = function(e) { text.style.left = e.clientX - left - 2 + "px"; text.style.top = Math.min(e.clientY - top - 2, maxTop) + "px"; }; move(e); if (e.type != "mousedown") return; if (host.renderer.$keepTextAreaAtCursor) host.renderer.$keepTextAreaAtCursor = null; // on windows context menu is opened after mouseup if (useragent.isWin) event.capture(host.container, move, onContextMenuClose); }; this.onContextMenuClose = onContextMenuClose; function onContextMenuClose() { setTimeout(function () { if (tempStyle) { text.style.cssText = tempStyle; tempStyle = ''; } if (host.renderer.$keepTextAreaAtCursor == null) { host.renderer.$keepTextAreaAtCursor = true; host.renderer.$moveTextAreaToCursor(); } }, 0); } // firefox fires contextmenu event after opening it if (!useragent.isGecko || useragent.isMac) { var onContextMenu = function(e) { host.textInput.onContextMenu(e); onContextMenuClose(); }; event.addListener(host.renderer.scroller, "contextmenu", onContextMenu); event.addListener(text, "contextmenu", onContextMenu); } }; exports.TextInput = TextInput; });