select.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699
  1. /* Functionality for finding, storing, and restoring selections
  2. *
  3. * This does not provide a generic API, just the minimal functionality
  4. * required by the CodeMirror system.
  5. */
  6. // Namespace object.
  7. var select = {};
  8. (function() {
  9. select.ie_selection = !(window.getSelection && document.createRange && document.createRange().endContainer);
  10. // Find the 'top-level' (defined as 'a direct child of the node
  11. // passed as the top argument') node that the given node is
  12. // contained in. Return null if the given node is not inside the top
  13. // node.
  14. function topLevelNodeAt(node, top) {
  15. while (node && node.parentNode != top)
  16. node = node.parentNode;
  17. return node;
  18. }
  19. // Find the top-level node that contains the node before this one.
  20. function topLevelNodeBefore(node, top) {
  21. while (!node.previousSibling && node.parentNode != top)
  22. node = node.parentNode;
  23. return topLevelNodeAt(node.previousSibling, top);
  24. }
  25. var fourSpaces = "\u00a0\u00a0\u00a0\u00a0";
  26. select.scrollToNode = function(node, cursor) {
  27. if (!node) return;
  28. var element = node, body = document.body,
  29. html = document.documentElement,
  30. atEnd = !element.nextSibling || !element.nextSibling.nextSibling
  31. || !element.nextSibling.nextSibling.nextSibling;
  32. // In Opera (and recent Webkit versions), BR elements *always*
  33. // have a offsetTop property of zero.
  34. var compensateHack = 0;
  35. while (element && !element.offsetTop) {
  36. compensateHack++;
  37. element = element.previousSibling;
  38. }
  39. // atEnd is another kludge for these browsers -- if the cursor is
  40. // at the end of the document, and the node doesn't have an
  41. // offset, just scroll to the end.
  42. if (compensateHack == 0) atEnd = false;
  43. // WebKit has a bad habit of (sometimes) happily returning bogus
  44. // offsets when the document has just been changed. This seems to
  45. // always be 5/5, so we don't use those.
  46. if (webkit && element && element.offsetTop == 5 && element.offsetLeft == 5)
  47. return;
  48. var y = compensateHack * (element ? element.offsetHeight : 0), x = 0,
  49. width = (node ? node.offsetWidth : 0), pos = element;
  50. while (pos && pos.offsetParent) {
  51. y += pos.offsetTop;
  52. // Don't count X offset for <br> nodes
  53. if (!isBR(pos))
  54. x += pos.offsetLeft;
  55. pos = pos.offsetParent;
  56. }
  57. var scroll_x = body.scrollLeft || html.scrollLeft || 0,
  58. scroll_y = body.scrollTop || html.scrollTop || 0,
  59. scroll = false, screen_width = window.innerWidth || html.clientWidth || 0;
  60. if (cursor || width < screen_width) {
  61. if (cursor) {
  62. var off = select.offsetInNode(node), size = nodeText(node).length;
  63. if (size) x += width * (off / size);
  64. }
  65. var screen_x = x - scroll_x;
  66. if (screen_x < 0 || screen_x > screen_width) {
  67. scroll_x = x;
  68. scroll = true;
  69. }
  70. }
  71. var screen_y = y - scroll_y;
  72. if (screen_y < 0 || atEnd || screen_y > (window.innerHeight || html.clientHeight || 0) - 50) {
  73. scroll_y = atEnd ? 1e6 : y;
  74. scroll = true;
  75. }
  76. if (scroll) window.scrollTo(scroll_x, scroll_y);
  77. };
  78. select.scrollToCursor = function(container) {
  79. select.scrollToNode(select.selectionTopNode(container, true) || container.firstChild, true);
  80. };
  81. // Used to prevent restoring a selection when we do not need to.
  82. var currentSelection = null;
  83. select.snapshotChanged = function() {
  84. if (currentSelection) currentSelection.changed = true;
  85. };
  86. // Find the 'leaf' node (BR or text) after the given one.
  87. function baseNodeAfter(node) {
  88. var next = node.nextSibling;
  89. if (next) {
  90. while (next.firstChild) next = next.firstChild;
  91. if (next.nodeType == 3 || isBR(next)) return next;
  92. else return baseNodeAfter(next);
  93. }
  94. else {
  95. var parent = node.parentNode;
  96. while (parent && !parent.nextSibling) parent = parent.parentNode;
  97. return parent && baseNodeAfter(parent);
  98. }
  99. }
  100. // This is called by the code in editor.js whenever it is replacing
  101. // a text node. The function sees whether the given oldNode is part
  102. // of the current selection, and updates this selection if it is.
  103. // Because nodes are often only partially replaced, the length of
  104. // the part that gets replaced has to be taken into account -- the
  105. // selection might stay in the oldNode if the newNode is smaller
  106. // than the selection's offset. The offset argument is needed in
  107. // case the selection does move to the new object, and the given
  108. // length is not the whole length of the new node (part of it might
  109. // have been used to replace another node).
  110. select.snapshotReplaceNode = function(from, to, length, offset) {
  111. if (!currentSelection) return;
  112. function replace(point) {
  113. if (from == point.node) {
  114. currentSelection.changed = true;
  115. if (length && point.offset > length) {
  116. point.offset -= length;
  117. }
  118. else {
  119. point.node = to;
  120. point.offset += (offset || 0);
  121. }
  122. }
  123. else if (select.ie_selection && point.offset == 0 && point.node == baseNodeAfter(from)) {
  124. currentSelection.changed = true;
  125. }
  126. }
  127. replace(currentSelection.start);
  128. replace(currentSelection.end);
  129. };
  130. select.snapshotMove = function(from, to, distance, relative, ifAtStart) {
  131. if (!currentSelection) return;
  132. function move(point) {
  133. if (from == point.node && (!ifAtStart || point.offset == 0)) {
  134. currentSelection.changed = true;
  135. point.node = to;
  136. if (relative) point.offset = Math.max(0, point.offset + distance);
  137. else point.offset = distance;
  138. }
  139. }
  140. move(currentSelection.start);
  141. move(currentSelection.end);
  142. };
  143. // Most functions are defined in two ways, one for the IE selection
  144. // model, one for the W3C one.
  145. if (select.ie_selection) {
  146. function selRange() {
  147. var sel = document.selection;
  148. if (!sel) return null;
  149. if (sel.createRange) return sel.createRange();
  150. else return sel.createTextRange();
  151. }
  152. function selectionNode(start) {
  153. var range = selRange();
  154. range.collapse(start);
  155. function nodeAfter(node) {
  156. var found = null;
  157. while (!found && node) {
  158. found = node.nextSibling;
  159. node = node.parentNode;
  160. }
  161. return nodeAtStartOf(found);
  162. }
  163. function nodeAtStartOf(node) {
  164. while (node && node.firstChild) node = node.firstChild;
  165. return {node: node, offset: 0};
  166. }
  167. var containing = range.parentElement();
  168. if (!isAncestor(document.body, containing)) return null;
  169. if (!containing.firstChild) return nodeAtStartOf(containing);
  170. var working = range.duplicate();
  171. working.moveToElementText(containing);
  172. working.collapse(true);
  173. for (var cur = containing.firstChild; cur; cur = cur.nextSibling) {
  174. if (cur.nodeType == 3) {
  175. var size = cur.nodeValue.length;
  176. working.move("character", size);
  177. }
  178. else {
  179. working.moveToElementText(cur);
  180. working.collapse(false);
  181. }
  182. var dir = range.compareEndPoints("StartToStart", working);
  183. if (dir == 0) return nodeAfter(cur);
  184. if (dir == 1) continue;
  185. if (cur.nodeType != 3) return nodeAtStartOf(cur);
  186. working.setEndPoint("StartToEnd", range);
  187. return {node: cur, offset: size - working.text.length};
  188. }
  189. return nodeAfter(containing);
  190. }
  191. select.markSelection = function() {
  192. currentSelection = null;
  193. var sel = document.selection;
  194. if (!sel) return;
  195. var start = selectionNode(true),
  196. end = selectionNode(false);
  197. if (!start || !end) return;
  198. currentSelection = {start: start, end: end, changed: false};
  199. };
  200. select.selectMarked = function() {
  201. if (!currentSelection || !currentSelection.changed) return;
  202. function makeRange(point) {
  203. var range = document.body.createTextRange(),
  204. node = point.node;
  205. if (!node) {
  206. range.moveToElementText(document.body);
  207. range.collapse(false);
  208. }
  209. else if (node.nodeType == 3) {
  210. range.moveToElementText(node.parentNode);
  211. var offset = point.offset;
  212. while (node.previousSibling) {
  213. node = node.previousSibling;
  214. offset += (node.innerText || "").length;
  215. }
  216. range.move("character", offset);
  217. }
  218. else {
  219. range.moveToElementText(node);
  220. range.collapse(true);
  221. }
  222. return range;
  223. }
  224. var start = makeRange(currentSelection.start), end = makeRange(currentSelection.end);
  225. start.setEndPoint("StartToEnd", end);
  226. start.select();
  227. };
  228. select.offsetInNode = function(node) {
  229. var range = selRange();
  230. if (!range) return 0;
  231. var range2 = range.duplicate();
  232. try {range2.moveToElementText(node);} catch(e){return 0;}
  233. range.setEndPoint("StartToStart", range2);
  234. return range.text.length;
  235. };
  236. // Get the top-level node that one end of the cursor is inside or
  237. // after. Note that this returns false for 'no cursor', and null
  238. // for 'start of document'.
  239. select.selectionTopNode = function(container, start) {
  240. var range = selRange();
  241. if (!range) return false;
  242. var range2 = range.duplicate();
  243. range.collapse(start);
  244. var around = range.parentElement();
  245. if (around && isAncestor(container, around)) {
  246. // Only use this node if the selection is not at its start.
  247. range2.moveToElementText(around);
  248. if (range.compareEndPoints("StartToStart", range2) == 1)
  249. return topLevelNodeAt(around, container);
  250. }
  251. // Move the start of a range to the start of a node,
  252. // compensating for the fact that you can't call
  253. // moveToElementText with text nodes.
  254. function moveToNodeStart(range, node) {
  255. if (node.nodeType == 3) {
  256. var count = 0, cur = node.previousSibling;
  257. while (cur && cur.nodeType == 3) {
  258. count += cur.nodeValue.length;
  259. cur = cur.previousSibling;
  260. }
  261. if (cur) {
  262. try{range.moveToElementText(cur);}
  263. catch(e){return false;}
  264. range.collapse(false);
  265. }
  266. else range.moveToElementText(node.parentNode);
  267. if (count) range.move("character", count);
  268. }
  269. else {
  270. try{range.moveToElementText(node);}
  271. catch(e){return false;}
  272. }
  273. return true;
  274. }
  275. // Do a binary search through the container object, comparing
  276. // the start of each node to the selection
  277. var start = 0, end = container.childNodes.length - 1;
  278. while (start < end) {
  279. var middle = Math.ceil((end + start) / 2), node = container.childNodes[middle];
  280. if (!node) return false; // Don't ask. IE6 manages this sometimes.
  281. if (!moveToNodeStart(range2, node)) return false;
  282. if (range.compareEndPoints("StartToStart", range2) == 1)
  283. start = middle;
  284. else
  285. end = middle - 1;
  286. }
  287. if (start == 0) {
  288. var test1 = selRange(), test2 = test1.duplicate();
  289. try {
  290. test2.moveToElementText(container);
  291. } catch(exception) {
  292. return null;
  293. }
  294. if (test1.compareEndPoints("StartToStart", test2) == 0)
  295. return null;
  296. }
  297. return container.childNodes[start] || null;
  298. };
  299. // Place the cursor after this.start. This is only useful when
  300. // manually moving the cursor instead of restoring it to its old
  301. // position.
  302. select.focusAfterNode = function(node, container) {
  303. var range = document.body.createTextRange();
  304. range.moveToElementText(node || container);
  305. range.collapse(!node);
  306. range.select();
  307. };
  308. select.somethingSelected = function() {
  309. var range = selRange();
  310. return range && (range.text != "");
  311. };
  312. function insertAtCursor(html) {
  313. var range = selRange();
  314. if (range) {
  315. range.pasteHTML(html);
  316. range.collapse(false);
  317. range.select();
  318. }
  319. }
  320. // Used to normalize the effect of the enter key, since browsers
  321. // do widely different things when pressing enter in designMode.
  322. select.insertNewlineAtCursor = function() {
  323. insertAtCursor("<br>");
  324. };
  325. select.insertTabAtCursor = function() {
  326. insertAtCursor(fourSpaces);
  327. };
  328. // Get the BR node at the start of the line on which the cursor
  329. // currently is, and the offset into the line. Returns null as
  330. // node if cursor is on first line.
  331. select.cursorPos = function(container, start) {
  332. var range = selRange();
  333. if (!range) return null;
  334. var topNode = select.selectionTopNode(container, start);
  335. while (topNode && !isBR(topNode))
  336. topNode = topNode.previousSibling;
  337. var range2 = range.duplicate();
  338. range.collapse(start);
  339. if (topNode) {
  340. range2.moveToElementText(topNode);
  341. range2.collapse(false);
  342. }
  343. else {
  344. // When nothing is selected, we can get all kinds of funky errors here.
  345. try { range2.moveToElementText(container); }
  346. catch (e) { return null; }
  347. range2.collapse(true);
  348. }
  349. range.setEndPoint("StartToStart", range2);
  350. return {node: topNode, offset: range.text.length};
  351. };
  352. select.setCursorPos = function(container, from, to) {
  353. function rangeAt(pos) {
  354. var range = document.body.createTextRange();
  355. if (!pos.node) {
  356. range.moveToElementText(container);
  357. range.collapse(true);
  358. }
  359. else {
  360. range.moveToElementText(pos.node);
  361. range.collapse(false);
  362. }
  363. range.move("character", pos.offset);
  364. return range;
  365. }
  366. var range = rangeAt(from);
  367. if (to && to != from)
  368. range.setEndPoint("EndToEnd", rangeAt(to));
  369. range.select();
  370. }
  371. // Some hacks for storing and re-storing the selection when the editor loses and regains focus.
  372. select.getBookmark = function (container) {
  373. var from = select.cursorPos(container, true), to = select.cursorPos(container, false);
  374. if (from && to) return {from: from, to: to};
  375. };
  376. // Restore a stored selection.
  377. select.setBookmark = function(container, mark) {
  378. if (!mark) return;
  379. select.setCursorPos(container, mark.from, mark.to);
  380. };
  381. }
  382. // W3C model
  383. else {
  384. // Find the node right at the cursor, not one of its
  385. // ancestors with a suitable offset. This goes down the DOM tree
  386. // until a 'leaf' is reached (or is it *up* the DOM tree?).
  387. function innerNode(node, offset) {
  388. while (node.nodeType != 3 && !isBR(node)) {
  389. var newNode = node.childNodes[offset] || node.nextSibling;
  390. offset = 0;
  391. while (!newNode && node.parentNode) {
  392. node = node.parentNode;
  393. newNode = node.nextSibling;
  394. }
  395. node = newNode;
  396. if (!newNode) break;
  397. }
  398. return {node: node, offset: offset};
  399. }
  400. // Store start and end nodes, and offsets within these, and refer
  401. // back to the selection object from those nodes, so that this
  402. // object can be updated when the nodes are replaced before the
  403. // selection is restored.
  404. select.markSelection = function () {
  405. var selection = window.getSelection();
  406. if (!selection || selection.rangeCount == 0)
  407. return (currentSelection = null);
  408. var range = selection.getRangeAt(0);
  409. currentSelection = {
  410. start: innerNode(range.startContainer, range.startOffset),
  411. end: innerNode(range.endContainer, range.endOffset),
  412. changed: false
  413. };
  414. };
  415. select.selectMarked = function () {
  416. var cs = currentSelection;
  417. // on webkit-based browsers, it is apparently possible that the
  418. // selection gets reset even when a node that is not one of the
  419. // endpoints get messed with. the most common situation where
  420. // this occurs is when a selection is deleted or overwitten. we
  421. // check for that here.
  422. function focusIssue() {
  423. if (cs.start.node == cs.end.node && cs.start.offset == cs.end.offset) {
  424. var selection = window.getSelection();
  425. if (!selection || selection.rangeCount == 0) return true;
  426. var range = selection.getRangeAt(0), point = innerNode(range.startContainer, range.startOffset);
  427. return cs.start.node != point.node || cs.start.offset != point.offset;
  428. }
  429. }
  430. if (!cs || !(cs.changed || (webkit && focusIssue()))) return;
  431. var range = document.createRange();
  432. function setPoint(point, which) {
  433. if (point.node) {
  434. // Some magic to generalize the setting of the start and end
  435. // of a range.
  436. if (point.offset == 0)
  437. range["set" + which + "Before"](point.node);
  438. else
  439. range["set" + which](point.node, point.offset);
  440. }
  441. else {
  442. range.setStartAfter(document.body.lastChild || document.body);
  443. }
  444. }
  445. setPoint(cs.end, "End");
  446. setPoint(cs.start, "Start");
  447. selectRange(range);
  448. };
  449. // Helper for selecting a range object.
  450. function selectRange(range) {
  451. var selection = window.getSelection();
  452. if (!selection) return;
  453. selection.removeAllRanges();
  454. selection.addRange(range);
  455. }
  456. function selectionRange() {
  457. var selection = window.getSelection();
  458. if (!selection || selection.rangeCount == 0)
  459. return false;
  460. else
  461. return selection.getRangeAt(0);
  462. }
  463. // Finding the top-level node at the cursor in the W3C is, as you
  464. // can see, quite an involved process.
  465. select.selectionTopNode = function(container, start) {
  466. var range = selectionRange();
  467. if (!range) return false;
  468. var node = start ? range.startContainer : range.endContainer;
  469. var offset = start ? range.startOffset : range.endOffset;
  470. // Work around (yet another) bug in Opera's selection model.
  471. if (window.opera && !start && range.endContainer == container && range.endOffset == range.startOffset + 1 &&
  472. container.childNodes[range.startOffset] && isBR(container.childNodes[range.startOffset]))
  473. offset--;
  474. // For text nodes, we look at the node itself if the cursor is
  475. // inside, or at the node before it if the cursor is at the
  476. // start.
  477. if (node.nodeType == 3){
  478. if (offset > 0)
  479. return topLevelNodeAt(node, container);
  480. else
  481. return topLevelNodeBefore(node, container);
  482. }
  483. // Occasionally, browsers will return the HTML node as
  484. // selection. If the offset is 0, we take the start of the frame
  485. // ('after null'), otherwise, we take the last node.
  486. else if (node.nodeName.toUpperCase() == "HTML") {
  487. return (offset == 1 ? null : container.lastChild);
  488. }
  489. // If the given node is our 'container', we just look up the
  490. // correct node by using the offset.
  491. else if (node == container) {
  492. return (offset == 0) ? null : node.childNodes[offset - 1];
  493. }
  494. // In any other case, we have a regular node. If the cursor is
  495. // at the end of the node, we use the node itself, if it is at
  496. // the start, we use the node before it, and in any other
  497. // case, we look up the child before the cursor and use that.
  498. else {
  499. if (offset == node.childNodes.length)
  500. return topLevelNodeAt(node, container);
  501. else if (offset == 0)
  502. return topLevelNodeBefore(node, container);
  503. else
  504. return topLevelNodeAt(node.childNodes[offset - 1], container);
  505. }
  506. };
  507. select.focusAfterNode = function(node, container) {
  508. var range = document.createRange();
  509. range.setStartBefore(container.firstChild || container);
  510. // In Opera, setting the end of a range at the end of a line
  511. // (before a BR) will cause the cursor to appear on the next
  512. // line, so we set the end inside of the start node when
  513. // possible.
  514. if (node && !node.firstChild)
  515. range.setEndAfter(node);
  516. else if (node)
  517. range.setEnd(node, node.childNodes.length);
  518. else
  519. range.setEndBefore(container.firstChild || container);
  520. range.collapse(false);
  521. selectRange(range);
  522. };
  523. select.somethingSelected = function() {
  524. var range = selectionRange();
  525. return range && !range.collapsed;
  526. };
  527. select.offsetInNode = function(node) {
  528. var range = selectionRange();
  529. if (!range) return 0;
  530. range = range.cloneRange();
  531. range.setStartBefore(node);
  532. return range.toString().length;
  533. };
  534. select.insertNodeAtCursor = function(node) {
  535. var range = selectionRange();
  536. if (!range) return;
  537. range.deleteContents();
  538. range.insertNode(node);
  539. webkitLastLineHack(document.body);
  540. // work around weirdness where Opera will magically insert a new
  541. // BR node when a BR node inside a span is moved around. makes
  542. // sure the BR ends up outside of spans.
  543. if (window.opera && isBR(node) && isSpan(node.parentNode)) {
  544. var next = node.nextSibling, p = node.parentNode, outer = p.parentNode;
  545. outer.insertBefore(node, p.nextSibling);
  546. var textAfter = "";
  547. for (; next && next.nodeType == 3; next = next.nextSibling) {
  548. textAfter += next.nodeValue;
  549. removeElement(next);
  550. }
  551. outer.insertBefore(makePartSpan(textAfter, document), node.nextSibling);
  552. }
  553. range = document.createRange();
  554. range.selectNode(node);
  555. range.collapse(false);
  556. selectRange(range);
  557. }
  558. select.insertNewlineAtCursor = function() {
  559. select.insertNodeAtCursor(document.createElement("BR"));
  560. };
  561. select.insertTabAtCursor = function() {
  562. select.insertNodeAtCursor(document.createTextNode(fourSpaces));
  563. };
  564. select.cursorPos = function(container, start) {
  565. var range = selectionRange();
  566. if (!range) return;
  567. var topNode = select.selectionTopNode(container, start);
  568. while (topNode && !isBR(topNode))
  569. topNode = topNode.previousSibling;
  570. range = range.cloneRange();
  571. range.collapse(start);
  572. if (topNode)
  573. range.setStartAfter(topNode);
  574. else
  575. range.setStartBefore(container);
  576. var text = range.toString();
  577. return {node: topNode, offset: text.length};
  578. };
  579. select.setCursorPos = function(container, from, to) {
  580. var range = document.createRange();
  581. function setPoint(node, offset, side) {
  582. if (offset == 0 && node && !node.nextSibling) {
  583. range["set" + side + "After"](node);
  584. return true;
  585. }
  586. if (!node)
  587. node = container.firstChild;
  588. else
  589. node = node.nextSibling;
  590. if (!node) return;
  591. if (offset == 0) {
  592. range["set" + side + "Before"](node);
  593. return true;
  594. }
  595. var backlog = []
  596. function decompose(node) {
  597. if (node.nodeType == 3)
  598. backlog.push(node);
  599. else
  600. forEach(node.childNodes, decompose);
  601. }
  602. while (true) {
  603. while (node && !backlog.length) {
  604. decompose(node);
  605. node = node.nextSibling;
  606. }
  607. var cur = backlog.shift();
  608. if (!cur) return false;
  609. var length = cur.nodeValue.length;
  610. if (length >= offset) {
  611. range["set" + side](cur, offset);
  612. return true;
  613. }
  614. offset -= length;
  615. }
  616. }
  617. to = to || from;
  618. if (setPoint(to.node, to.offset, "End") && setPoint(from.node, from.offset, "Start"))
  619. selectRange(range);
  620. };
  621. }
  622. })();