textarea.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547
  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, loader) {
  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. // Power up ace on the textarea:
  194. var options = {};
  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, loader);
  203. // Create the setting's panel.
  204. setupSettingPanel(settingDiv, settingOpener, editor, options);
  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.setOption = function(key, value) {
  262. if (options[key] == value) return;
  263. switch (key) {
  264. case "mode":
  265. if (value != "text") {
  266. // Load the required mode file. Files get loaded only once.
  267. loader("mode-" + value + ".js", "ace/mode/" + value, function() {
  268. var aceMode = require("../mode/" + value).Mode;
  269. session.setMode(new aceMode());
  270. });
  271. } else {
  272. session.setMode(new (require("../mode/text").Mode));
  273. }
  274. break;
  275. case "theme":
  276. if (value != "textmate") {
  277. // Load the required theme file. Files get loaded only once.
  278. loader("theme-" + value + ".js", "ace/theme/" + value, function() {
  279. editor.setTheme("ace/theme/" + value);
  280. });
  281. } else {
  282. editor.setTheme("ace/theme/textmate");
  283. }
  284. break;
  285. case "fontSize":
  286. editorDiv.style.fontSize = value;
  287. break;
  288. case "keybindings":
  289. switch (value) {
  290. case "vim":
  291. editor.setKeyboardHandler("ace/keyboard/vim");
  292. break;
  293. case "emacs":
  294. editor.setKeyboardHandler("ace/keyboard/emacs");
  295. break;
  296. default:
  297. editor.setKeyboardHandler(null);
  298. }
  299. break;
  300. case "softWrap":
  301. switch (value) {
  302. case "off":
  303. session.setUseWrapMode(false);
  304. renderer.setPrintMarginColumn(80);
  305. break;
  306. case "40":
  307. session.setUseWrapMode(true);
  308. session.setWrapLimitRange(40, 40);
  309. renderer.setPrintMarginColumn(40);
  310. break;
  311. case "80":
  312. session.setUseWrapMode(true);
  313. session.setWrapLimitRange(80, 80);
  314. renderer.setPrintMarginColumn(80);
  315. break;
  316. case "free":
  317. session.setUseWrapMode(true);
  318. session.setWrapLimitRange(null, null);
  319. renderer.setPrintMarginColumn(80);
  320. break;
  321. }
  322. break;
  323. default:
  324. editor.$setOption(key, toBool(value));
  325. }
  326. options[key] = value;
  327. };
  328. editor.getOption = function(key) {
  329. return options[key];
  330. };
  331. editor.getOptions = function() {
  332. return options;
  333. };
  334. editor.setOptions(exports.options);
  335. return editor;
  336. }
  337. function setupSettingPanel(settingDiv, settingOpener, editor, options) {
  338. var BOOL = null;
  339. var desc = {
  340. mode: "Mode:",
  341. gutter: "Display Gutter:",
  342. theme: "Theme:",
  343. fontSize: "Font Size:",
  344. softWrap: "Soft Wrap:",
  345. keybindings: "Keyboard",
  346. showPrintMargin: "Show Print Margin:",
  347. useSoftTabs: "Use Soft Tabs:",
  348. showInvisibles: "Show Invisibles"
  349. };
  350. var optionValues = {
  351. mode: {
  352. text: "Plain",
  353. javascript: "JavaScript",
  354. xml: "XML",
  355. html: "HTML",
  356. css: "CSS",
  357. scss: "SCSS",
  358. python: "Python",
  359. php: "PHP",
  360. java: "Java",
  361. ruby: "Ruby",
  362. c_cpp: "C/C++",
  363. coffee: "CoffeeScript",
  364. json: "json",
  365. perl: "Perl",
  366. clojure: "Clojure",
  367. ocaml: "OCaml",
  368. csharp: "C#",
  369. haxe: "haXe",
  370. svg: "SVG",
  371. textile: "Textile",
  372. groovy: "Groovy",
  373. liquid: "Liquid",
  374. Scala: "Scala"
  375. },
  376. theme: {
  377. clouds: "Clouds",
  378. clouds_midnight: "Clouds Midnight",
  379. cobalt: "Cobalt",
  380. crimson_editor: "Crimson Editor",
  381. dawn: "Dawn",
  382. eclipse: "Eclipse",
  383. idle_fingers: "Idle Fingers",
  384. kr_theme: "Kr Theme",
  385. merbivore: "Merbivore",
  386. merbivore_soft: "Merbivore Soft",
  387. mono_industrial: "Mono Industrial",
  388. monokai: "Monokai",
  389. pastel_on_dark: "Pastel On Dark",
  390. solarized_dark: "Solarized Dark",
  391. solarized_light: "Solarized Light",
  392. textmate: "Textmate",
  393. twilight: "Twilight",
  394. vibrant_ink: "Vibrant Ink"
  395. },
  396. gutter: BOOL,
  397. fontSize: {
  398. "10px": "10px",
  399. "11px": "11px",
  400. "12px": "12px",
  401. "14px": "14px",
  402. "16px": "16px"
  403. },
  404. softWrap: {
  405. off: "Off",
  406. 40: "40",
  407. 80: "80",
  408. free: "Free"
  409. },
  410. keybindings: {
  411. ace: "ace",
  412. vim: "vim",
  413. emacs: "emacs"
  414. },
  415. showPrintMargin: BOOL,
  416. useSoftTabs: BOOL,
  417. showInvisibles: BOOL
  418. };
  419. var table = [];
  420. table.push("<table><tr><th>Setting</th><th>Value</th></tr>");
  421. function renderOption(builder, option, obj, cValue) {
  422. if (!obj) {
  423. builder.push(
  424. "<input type='checkbox' title='", option, "' ",
  425. cValue == "true" ? "checked='true'" : "",
  426. "'></input>"
  427. );
  428. return;
  429. }
  430. builder.push("<select title='" + option + "'>");
  431. for (var value in obj) {
  432. builder.push("<option value='" + value + "' ");
  433. if (cValue == value) {
  434. builder.push(" selected ");
  435. }
  436. builder.push(">",
  437. obj[value],
  438. "</option>");
  439. }
  440. builder.push("</select>");
  441. }
  442. for (var option in options) {
  443. table.push("<tr><td>", desc[option], "</td>");
  444. table.push("<td>");
  445. renderOption(table, option, optionValues[option], options[option]);
  446. table.push("</td></tr>");
  447. }
  448. table.push("</table>");
  449. settingDiv.innerHTML = table.join("");
  450. var onChange = function(e) {
  451. var select = e.currentTarget;
  452. editor.setOption(select.title, select.value);
  453. };
  454. var onClick = function(e) {
  455. var cb = e.currentTarget;
  456. editor.setOption(cb.title, cb.checked);
  457. };
  458. var selects = settingDiv.getElementsByTagName("select");
  459. for (var i = 0; i < selects.length; i++)
  460. selects[i].onchange = onChange;
  461. var cbs = settingDiv.getElementsByTagName("input");
  462. for (var i = 0; i < cbs.length; i++)
  463. cbs[i].onclick = onClick;
  464. var button = document.createElement("input");
  465. button.type = "button";
  466. button.value = "Hide";
  467. event.addListener(button, "click", function() {
  468. editor.setDisplaySettings(false);
  469. });
  470. settingDiv.appendChild(button);
  471. settingDiv.hideButton = button;
  472. }
  473. // Default startup options.
  474. exports.options = {
  475. mode: "text",
  476. theme: "textmate",
  477. gutter: "false",
  478. fontSize: "12px",
  479. softWrap: "off",
  480. keybindings: "ace",
  481. showPrintMargin: "false",
  482. useSoftTabs: "true",
  483. showInvisibles: "false"
  484. };
  485. });