textarea.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521
  1. /* ***** BEGIN LICENSE BLOCK *****
  2. * Distributed under the BSD license:
  3. *
  4. * Copyright (c) 2010, Ajax.org B.V.
  5. * All rights reserved.
  6. *
  7. * Redistribution and use in source and binary forms, with or without
  8. * modification, are permitted provided that the following conditions are met:
  9. * * Redistributions of source code must retain the above copyright
  10. * notice, this list of conditions and the following disclaimer.
  11. * * Redistributions in binary form must reproduce the above copyright
  12. * notice, this list of conditions and the following disclaimer in the
  13. * documentation and/or other materials provided with the distribution.
  14. * * Neither the name of Ajax.org B.V. nor the
  15. * names of its contributors may be used to endorse or promote products
  16. * derived from this software without specific prior written permission.
  17. *
  18. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
  19. * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  20. * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  21. * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY
  22. * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  23. * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  24. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  25. * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  26. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  27. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  28. *
  29. * ***** END LICENSE BLOCK ***** */
  30. define(function(require, exports, module) {
  31. "use strict";
  32. var event = require("../lib/event");
  33. var UA = require("../lib/useragent");
  34. var net = require("../lib/net");
  35. var ace = require("../ace");
  36. require("../theme/textmate");
  37. module.exports = exports = ace;
  38. /*
  39. * Returns the CSS property of element.
  40. * 1) If the CSS property is on the style object of the element, use it, OR
  41. * 2) Compute the CSS property
  42. *
  43. * If the property can't get computed, is 'auto' or 'intrinsic', the former
  44. * calculated property is used (this can happen in cases where the textarea
  45. * is hidden and has no dimension styles).
  46. */
  47. var getCSSProperty = function(element, container, property) {
  48. var ret = element.style[property];
  49. if (!ret) {
  50. if (window.getComputedStyle) {
  51. ret = window.getComputedStyle(element, '').getPropertyValue(property);
  52. } else {
  53. ret = element.currentStyle[property];
  54. }
  55. }
  56. if (!ret || ret == 'auto' || ret == 'intrinsic') {
  57. ret = container.style[property];
  58. }
  59. return ret;
  60. };
  61. function applyStyles(elm, styles) {
  62. for (var style in styles) {
  63. elm.style[style] = styles[style];
  64. }
  65. }
  66. function setupContainer(element, getValue) {
  67. if (element.type != 'textarea') {
  68. throw new Error("Textarea required!");
  69. }
  70. var parentNode = element.parentNode;
  71. // This will hold the editor.
  72. var container = document.createElement('div');
  73. // To put Ace in the place of the textarea, we have to copy a few of the
  74. // textarea's style attributes to the div container.
  75. //
  76. // The problem is that the properties have to get computed (they might be
  77. // defined by a CSS file on the page - you can't access such rules that
  78. // apply to an element via elm.style). Computed properties are converted to
  79. // pixels although the dimension might be given as percentage. When the
  80. // window resizes, the dimensions defined by percentages changes, so the
  81. // properties have to get recomputed to get the new/true pixels.
  82. var resizeEvent = function() {
  83. var style = 'position:relative;';
  84. [
  85. 'margin-top', 'margin-left', 'margin-right', 'margin-bottom'
  86. ].forEach(function(item) {
  87. style += item + ':' +
  88. getCSSProperty(element, container, item) + ';';
  89. });
  90. // Calculating the width/height of the textarea is somewhat tricky. To
  91. // do it right, you have to include the paddings to the sides as well
  92. // (eg. width = width + padding-left, -right). This works well, as
  93. // long as the width of the element is not set or given in pixels. In
  94. // this case and after the textarea is hidden, getCSSProperty(element,
  95. // container, 'width') will still return pixel value. If the element
  96. // has realtiv dimensions (e.g. width='95<percent>')
  97. // getCSSProperty(...) will return pixel values only as long as the
  98. // textarea is visible. After it is hidden getCSSProperty will return
  99. // the relative dimensions as they are set on the element (in the case
  100. // of width, 95<percent>).
  101. // Making the sum of pixel vaules (e.g. padding) and realtive values
  102. // (e.g. <percent>) is not possible. As such the padding styles are
  103. // ignored.
  104. // The complete width is the width of the textarea + the padding
  105. // to the left and right.
  106. var width = getCSSProperty(element, container, 'width') || (element.clientWidth + "px");
  107. var height = getCSSProperty(element, container, 'height') || (element.clientHeight + "px");
  108. style += 'height:' + height + ';width:' + width + ';';
  109. // Set the display property to 'inline-block'.
  110. style += 'display:inline-block;';
  111. container.setAttribute('style', style);
  112. };
  113. event.addListener(window, 'resize', resizeEvent);
  114. // Call the resizeEvent once, so that the size of the container is
  115. // calculated.
  116. resizeEvent();
  117. // Insert the div container after the element.
  118. parentNode.insertBefore(container, element.nextSibling);
  119. // Override the forms onsubmit function. Set the innerHTML and value
  120. // of the textarea before submitting.
  121. while (parentNode !== document) {
  122. if (parentNode.tagName.toUpperCase() === 'FORM') {
  123. var oldSumit = parentNode.onsubmit;
  124. // Override the onsubmit function of the form.
  125. parentNode.onsubmit = function(evt) {
  126. element.value = getValue();
  127. // If there is a onsubmit function already, then call
  128. // it with the current context and pass the event.
  129. if (oldSumit) {
  130. oldSumit.call(this, evt);
  131. }
  132. };
  133. break;
  134. }
  135. parentNode = parentNode.parentNode;
  136. }
  137. return container;
  138. }
  139. exports.transformTextarea = function(element, options) {
  140. var session;
  141. var container = setupContainer(element, function() {
  142. return session.getValue();
  143. });
  144. // Hide the element.
  145. element.style.display = 'none';
  146. container.style.background = 'white';
  147. //
  148. var editorDiv = document.createElement("div");
  149. applyStyles(editorDiv, {
  150. top: "0px",
  151. left: "0px",
  152. right: "0px",
  153. bottom: "0px",
  154. border: "1px solid gray",
  155. position: "absolute"
  156. });
  157. container.appendChild(editorDiv);
  158. var settingOpener = document.createElement("div");
  159. applyStyles(settingOpener, {
  160. position: "absolute",
  161. right: "0px",
  162. bottom: "0px",
  163. background: "red",
  164. cursor: "nw-resize",
  165. borderStyle: "solid",
  166. borderWidth: "9px 8px 10px 9px",
  167. width: "2px",
  168. borderColor: "lightblue gray gray lightblue",
  169. zIndex: 101
  170. });
  171. var settingDiv = document.createElement("div");
  172. var settingDivStyles = {
  173. top: "0px",
  174. left: "20%",
  175. right: "0px",
  176. bottom: "0px",
  177. position: "absolute",
  178. padding: "5px",
  179. zIndex: 100,
  180. color: "white",
  181. display: "none",
  182. overflow: "auto",
  183. fontSize: "14px",
  184. boxShadow: "-5px 2px 3px gray"
  185. };
  186. if (!UA.isOldIE) {
  187. settingDivStyles.backgroundColor = "rgba(0, 0, 0, 0.6)";
  188. } else {
  189. settingDivStyles.backgroundColor = "#333";
  190. }
  191. applyStyles(settingDiv, settingDivStyles);
  192. container.appendChild(settingDiv);
  193. options = options || exports.defaultOptions;
  194. // Power up ace on the textarea:
  195. var editor = ace.edit(editorDiv);
  196. session = editor.getSession();
  197. session.setValue(element.value || element.innerHTML);
  198. editor.focus();
  199. // Add the settingPanel opener to the editor's div.
  200. container.appendChild(settingOpener);
  201. // Create the API.
  202. setupApi(editor, editorDiv, settingDiv, ace, options, load);
  203. // Create the setting's panel.
  204. setupSettingPanel(settingDiv, settingOpener, editor);
  205. var state = "";
  206. event.addListener(settingOpener, "mousemove", function(e) {
  207. var rect = this.getBoundingClientRect();
  208. var x = e.clientX - rect.left, y = e.clientY - rect.top;
  209. if (x + y < (rect.width + rect.height)/2) {
  210. this.style.cursor = "pointer";
  211. state = "toggle";
  212. } else {
  213. state = "resize";
  214. this.style.cursor = "nw-resize";
  215. }
  216. });
  217. event.addListener(settingOpener, "mousedown", function(e) {
  218. if (state == "toggle") {
  219. editor.setDisplaySettings();
  220. return;
  221. }
  222. container.style.zIndex = 100000;
  223. var rect = container.getBoundingClientRect();
  224. var startX = rect.width + rect.left - e.clientX;
  225. var startY = rect.height + rect.top - e.clientY;
  226. event.capture(settingOpener, function(e) {
  227. container.style.width = e.clientX - rect.left + startX + "px";
  228. container.style.height = e.clientY - rect.top + startY + "px";
  229. editor.resize();
  230. }, function() {});
  231. });
  232. return editor;
  233. };
  234. function load(url, module, callback) {
  235. net.loadScript(url, function() {
  236. require([module], callback);
  237. });
  238. }
  239. function setupApi(editor, editorDiv, settingDiv, ace, options, loader) {
  240. var session = editor.getSession();
  241. var renderer = editor.renderer;
  242. loader = loader || load;
  243. function toBool(value) {
  244. return value === "true" || value == true;
  245. }
  246. editor.setDisplaySettings = function(display) {
  247. if (display == null)
  248. display = settingDiv.style.display == "none";
  249. if (display) {
  250. settingDiv.style.display = "block";
  251. settingDiv.hideButton.focus();
  252. editor.on("focus", function onFocus() {
  253. editor.removeListener("focus", onFocus);
  254. settingDiv.style.display = "none";
  255. });
  256. } else {
  257. editor.focus();
  258. }
  259. };
  260. editor.$setOption = editor.setOption;
  261. editor.$getOption = editor.getOption;
  262. editor.setOption = function(key, value) {
  263. switch (key) {
  264. case "mode":
  265. editor.$setOption("mode", "ace/mode/" + value)
  266. break;
  267. case "theme":
  268. editor.$setOption("theme", "ace/theme/" + value)
  269. break;
  270. case "keybindings":
  271. switch (value) {
  272. case "vim":
  273. editor.setKeyboardHandler("ace/keyboard/vim");
  274. break;
  275. case "emacs":
  276. editor.setKeyboardHandler("ace/keyboard/emacs");
  277. break;
  278. default:
  279. editor.setKeyboardHandler(null);
  280. }
  281. break;
  282. case "softWrap":
  283. case "fontSize":
  284. editor.$setOption(key, value);
  285. break;
  286. default:
  287. editor.$setOption(key, toBool(value));
  288. }
  289. };
  290. editor.getOption = function(key) {
  291. switch (key) {
  292. case "mode":
  293. return editor.$getOption("mode").substr("ace/mode/".length)
  294. break;
  295. case "theme":
  296. return editor.$getOption("theme").substr("ace/theme/".length)
  297. break;
  298. case "keybindings":
  299. var value = editor.getKeyboardHandler()
  300. switch (value && value.$id) {
  301. case "ace/keyboard/vim":
  302. return "vim";
  303. case "ace/keyboard/emacs":
  304. return "emacs";
  305. default:
  306. return "ace";
  307. }
  308. break;
  309. default:
  310. return editor.$getOption(key);
  311. }
  312. };
  313. editor.setOptions(options);
  314. return editor;
  315. }
  316. function setupSettingPanel(settingDiv, settingOpener, editor) {
  317. var BOOL = null;
  318. var desc = {
  319. mode: "Mode:",
  320. wrap: "Soft Wrap:",
  321. theme: "Theme:",
  322. fontSize: "Font Size:",
  323. showGutter: "Display Gutter:",
  324. keybindings: "Keyboard",
  325. showPrintMargin: "Show Print Margin:",
  326. useSoftTabs: "Use Soft Tabs:",
  327. showInvisibles: "Show Invisibles"
  328. };
  329. var optionValues = {
  330. mode: {
  331. text: "Plain",
  332. javascript: "JavaScript",
  333. xml: "XML",
  334. html: "HTML",
  335. css: "CSS",
  336. scss: "SCSS",
  337. python: "Python",
  338. php: "PHP",
  339. java: "Java",
  340. ruby: "Ruby",
  341. c_cpp: "C/C++",
  342. coffee: "CoffeeScript",
  343. json: "json",
  344. perl: "Perl",
  345. clojure: "Clojure",
  346. ocaml: "OCaml",
  347. csharp: "C#",
  348. haxe: "haXe",
  349. svg: "SVG",
  350. textile: "Textile",
  351. groovy: "Groovy",
  352. liquid: "Liquid",
  353. Scala: "Scala"
  354. },
  355. theme: {
  356. clouds: "Clouds",
  357. clouds_midnight: "Clouds Midnight",
  358. cobalt: "Cobalt",
  359. crimson_editor: "Crimson Editor",
  360. dawn: "Dawn",
  361. eclipse: "Eclipse",
  362. idle_fingers: "Idle Fingers",
  363. kr_theme: "Kr Theme",
  364. merbivore: "Merbivore",
  365. merbivore_soft: "Merbivore Soft",
  366. mono_industrial: "Mono Industrial",
  367. monokai: "Monokai",
  368. pastel_on_dark: "Pastel On Dark",
  369. solarized_dark: "Solarized Dark",
  370. solarized_light: "Solarized Light",
  371. textmate: "Textmate",
  372. twilight: "Twilight",
  373. vibrant_ink: "Vibrant Ink"
  374. },
  375. showGutter: BOOL,
  376. fontSize: {
  377. "10px": "10px",
  378. "11px": "11px",
  379. "12px": "12px",
  380. "14px": "14px",
  381. "16px": "16px"
  382. },
  383. wrap: {
  384. off: "Off",
  385. 40: "40",
  386. 80: "80",
  387. free: "Free"
  388. },
  389. keybindings: {
  390. ace: "ace",
  391. vim: "vim",
  392. emacs: "emacs"
  393. },
  394. showPrintMargin: BOOL,
  395. useSoftTabs: BOOL,
  396. showInvisibles: BOOL
  397. };
  398. var table = [];
  399. table.push("<table><tr><th>Setting</th><th>Value</th></tr>");
  400. function renderOption(builder, option, obj, cValue) {
  401. if (!obj) {
  402. builder.push(
  403. "<input type='checkbox' title='", option, "' ",
  404. cValue + "" == "true" ? "checked='true'" : "",
  405. "'></input>"
  406. );
  407. return;
  408. }
  409. builder.push("<select title='" + option + "'>");
  410. for (var value in obj) {
  411. builder.push("<option value='" + value + "' ");
  412. if (cValue == value) {
  413. builder.push(" selected ");
  414. }
  415. builder.push(">",
  416. obj[value],
  417. "</option>");
  418. }
  419. builder.push("</select>");
  420. }
  421. for (var option in exports.defaultOptions) {
  422. table.push("<tr><td>", desc[option], "</td>");
  423. table.push("<td>");
  424. renderOption(table, option, optionValues[option], editor.getOption(option));
  425. table.push("</td></tr>");
  426. }
  427. table.push("</table>");
  428. settingDiv.innerHTML = table.join("");
  429. var onChange = function(e) {
  430. var select = e.currentTarget;
  431. editor.setOption(select.title, select.value);
  432. };
  433. var onClick = function(e) {
  434. var cb = e.currentTarget;
  435. editor.setOption(cb.title, cb.checked);
  436. };
  437. var selects = settingDiv.getElementsByTagName("select");
  438. for (var i = 0; i < selects.length; i++)
  439. selects[i].onchange = onChange;
  440. var cbs = settingDiv.getElementsByTagName("input");
  441. for (var i = 0; i < cbs.length; i++)
  442. cbs[i].onclick = onClick;
  443. var button = document.createElement("input");
  444. button.type = "button";
  445. button.value = "Hide";
  446. event.addListener(button, "click", function() {
  447. editor.setDisplaySettings(false);
  448. });
  449. settingDiv.appendChild(button);
  450. settingDiv.hideButton = button;
  451. }
  452. // Default startup options.
  453. exports.defaultOptions = {
  454. mode: "javascript",
  455. theme: "textmate",
  456. wrap: "off",
  457. fontSize: "12px",
  458. showGutter: "false",
  459. keybindings: "ace",
  460. showPrintMargin: "false",
  461. useSoftTabs: "true",
  462. showInvisibles: "false"
  463. };
  464. });