forms.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850
  1. /* This Source Code Form is subject to the terms of the Mozilla Public
  2. * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  3. * You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. define(["jquery", "util", "session", "elementFinder", "eventMaker", "templating", "ot"], function ($, util, session, elementFinder, eventMaker, templating, ot) {
  5. var forms = util.Module("forms");
  6. var assert = util.assert;
  7. // This is how much larger the focus element is than the element it surrounds
  8. // (this is padding on each side)
  9. var FOCUS_BUFFER = 5;
  10. var inRemoteUpdate = false;
  11. function suppressSync(element) {
  12. var ignoreForms = TogetherJS.config.get("ignoreForms");
  13. if (ignoreForms === true) {
  14. return true;
  15. }
  16. else {
  17. return $(element).is(ignoreForms.join(","));
  18. }
  19. }
  20. function maybeChange(event) {
  21. // Called when we get an event that may or may not indicate a real change
  22. // (like keyup in a textarea)
  23. var tag = event.target.tagName;
  24. if (tag == "TEXTAREA" || tag == "INPUT") {
  25. change(event);
  26. }
  27. }
  28. function change(event) {
  29. sendData({
  30. element: event.target,
  31. value: getValue(event.target)
  32. });
  33. }
  34. function sendData(attrs) {
  35. var el = $(attrs.element);
  36. assert(el);
  37. var tracker = attrs.tracker;
  38. var value = attrs.value;
  39. if (inRemoteUpdate) {
  40. return;
  41. }
  42. if (elementFinder.ignoreElement(el) ||
  43. (elementTracked(el) && !tracker) ||
  44. suppressSync(el)) {
  45. return;
  46. }
  47. var location = elementFinder.elementLocation(el);
  48. var msg = {
  49. type: "form-update",
  50. element: location
  51. };
  52. if (isText(el) || tracker) {
  53. var history = el.data("togetherjsHistory");
  54. if (history) {
  55. if (history.current == value) {
  56. return;
  57. }
  58. var delta = ot.TextReplace.fromChange(history.current, value);
  59. assert(delta);
  60. history.add(delta);
  61. maybeSendUpdate(msg.element, history, tracker);
  62. return;
  63. } else {
  64. msg.value = value;
  65. msg.basis = 1;
  66. el.data("togetherjsHistory", ot.SimpleHistory(session.clientId, value, 1));
  67. }
  68. } else {
  69. msg.value = value;
  70. }
  71. session.send(msg);
  72. }
  73. function isCheckable(el) {
  74. el = $(el);
  75. var type = (el.prop("type") || "text").toLowerCase();
  76. if (el.prop("tagName") == "INPUT" && ["radio", "checkbox"].indexOf(type) != -1) {
  77. return true;
  78. }
  79. return false;
  80. }
  81. var editTrackers = {};
  82. var liveTrackers = [];
  83. TogetherJS.addTracker = function (TrackerClass, skipSetInit) {
  84. assert(typeof TrackerClass === "function", "You must pass in a class");
  85. assert(typeof TrackerClass.prototype.trackerName === "string",
  86. "Needs a .prototype.trackerName string");
  87. // Test for required instance methods.
  88. "destroy update init makeInit tracked".split(/ /).forEach(function(m) {
  89. assert(typeof TrackerClass.prototype[m] === "function",
  90. "Missing required tracker method: "+m);
  91. });
  92. // Test for required class methods.
  93. "scan tracked".split(/ /).forEach(function(m) {
  94. assert(typeof TrackerClass[m] === "function",
  95. "Missing required tracker class method: "+m);
  96. });
  97. editTrackers[TrackerClass.prototype.trackerName] = TrackerClass;
  98. if (!skipSetInit) {
  99. setInit();
  100. }
  101. };
  102. var AceEditor = util.Class({
  103. trackerName: "AceEditor",
  104. constructor: function (el) {
  105. this.element = $(el)[0];
  106. assert($(this.element).hasClass("ace_editor"));
  107. this._change = this._change.bind(this);
  108. this._editor().document.on("change", this._change);
  109. },
  110. tracked: function (el) {
  111. return this.element === $(el)[0];
  112. },
  113. destroy: function (el) {
  114. this._editor().document.removeListener("change", this._change);
  115. },
  116. update: function (msg) {
  117. this._editor().document.setValue(msg.value);
  118. },
  119. init: function (update, msg) {
  120. this.update(update);
  121. },
  122. makeInit: function () {
  123. return {
  124. element: this.element,
  125. tracker: this.trackerName,
  126. value: this._editor().document.getValue()
  127. };
  128. },
  129. _editor: function () {
  130. return this.element.env;
  131. },
  132. _change: function (e) {
  133. // FIXME: I should have an internal .send() function that automatically
  134. // asserts !inRemoteUpdate, among other things
  135. if (inRemoteUpdate) {
  136. return;
  137. }
  138. sendData({
  139. tracker: this.trackerName,
  140. element: this.element,
  141. value: this.getContent()
  142. });
  143. },
  144. getContent: function() {
  145. return this._editor().document.getValue();
  146. }
  147. });
  148. AceEditor.scan = function () {
  149. return $(".ace_editor");
  150. };
  151. AceEditor.tracked = function (el) {
  152. return !! $(el).closest(".ace_editor").length;
  153. };
  154. TogetherJS.addTracker(AceEditor, true /* skip setInit */);
  155. var CodeMirrorEditor = util.Class({
  156. trackerName: "CodeMirrorEditor",
  157. constructor: function (el) {
  158. this.element = $(el)[0];
  159. assert(this.element.CodeMirror);
  160. this._change = this._change.bind(this);
  161. this._editor().on("change", this._change);
  162. },
  163. tracked: function (el) {
  164. return this.element === $(el)[0];
  165. },
  166. destroy: function (el) {
  167. this._editor().off("change", this._change);
  168. },
  169. update: function (msg) {
  170. this._editor().setValue(msg.value);
  171. },
  172. init: function (msg) {
  173. if (msg.value) {
  174. this.update(msg);
  175. }
  176. },
  177. makeInit: function () {
  178. return {
  179. element: this.element,
  180. tracker: this.trackerName,
  181. value: this._editor().getValue()
  182. };
  183. },
  184. _change: function (editor, change) {
  185. if (inRemoteUpdate) {
  186. return;
  187. }
  188. sendData({
  189. tracker: this.trackerName,
  190. element: this.element,
  191. value: this.getContent()
  192. });
  193. },
  194. _editor: function () {
  195. return this.element.CodeMirror;
  196. },
  197. getContent: function() {
  198. return this._editor().getValue();
  199. }
  200. });
  201. CodeMirrorEditor.scan = function () {
  202. var result = [];
  203. var els = document.body.getElementsByTagName("*");
  204. var _len = els.length;
  205. for (var i=0; i<_len; i++) {
  206. var el = els[i];
  207. if (el.CodeMirror) {
  208. result.push(el);
  209. }
  210. }
  211. return $(result);
  212. };
  213. CodeMirrorEditor.tracked = function (el) {
  214. el = $(el)[0];
  215. while (el) {
  216. if (el.CodeMirror) {
  217. return true;
  218. }
  219. el = el.parentNode;
  220. }
  221. return false;
  222. };
  223. TogetherJS.addTracker(CodeMirrorEditor, true /* skip setInit */);
  224. var CKEditor = util.Class({
  225. trackerName: "CKEditor",
  226. constructor: function (el) {
  227. this.element = $(el)[0];
  228. assert(CKEDITOR);
  229. assert(CKEDITOR.dom.element.get(this.element));
  230. this._change = this._change.bind(this);
  231. // FIXME: change event is available since CKEditor 4.2
  232. this._editor().on("change", this._change);
  233. },
  234. tracked: function (el) {
  235. return this.element === $(el)[0];
  236. },
  237. destroy: function (el) {
  238. this._editor().removeListener("change", this._change);
  239. },
  240. update: function (msg) {
  241. //FIXME: use setHtml instead of setData to avoid frame reloading overhead
  242. this._editor().editable().setHtml(msg.value);
  243. },
  244. init: function (update, msg) {
  245. this.update(update);
  246. },
  247. makeInit: function () {
  248. return {
  249. element: this.element,
  250. tracker: this.trackerName,
  251. value: this.getContent()
  252. };
  253. },
  254. _change: function (e) {
  255. if (inRemoteUpdate) {
  256. return;
  257. }
  258. sendData({
  259. tracker: this.trackerName,
  260. element: this.element,
  261. value: this.getContent()
  262. });
  263. },
  264. _editor: function () {
  265. return CKEDITOR.dom.element.get(this.element).getEditor();
  266. },
  267. getContent: function () {
  268. return this._editor().getData();
  269. }
  270. });
  271. CKEditor.scan = function () {
  272. var result = [];
  273. if (typeof CKEDITOR == "undefined") {
  274. return;
  275. }
  276. var editorInstance;
  277. for (var instanceIdentifier in CKEDITOR.instances) {
  278. editorInstance = document.getElementById(instanceIdentifier) || document.getElementsByName(instanceIdentifier)[0];
  279. if (editorInstance) {
  280. result.push(editorInstance);
  281. }
  282. }
  283. return $(result);
  284. };
  285. CKEditor.tracked = function (el) {
  286. if (typeof CKEDITOR == "undefined") {
  287. return false;
  288. }
  289. el = $(el)[0];
  290. return !! (CKEDITOR.dom.element.get(el) && CKEDITOR.dom.element.get(el).getEditor());
  291. };
  292. TogetherJS.addTracker(CKEditor, true /* skip setInit */);
  293. //////////////////// BEGINNING OF TINYMCE ////////////////////////
  294. var tinymceEditor = util.Class({
  295. trackerName: "tinymceEditor",
  296. constructor: function (el) {
  297. this.element = $(el)[0];
  298. assert($(this.element).attr('id').indexOf('mce_') != -1);
  299. this._change = this._change.bind(this);
  300. this._editor().on("input keyup cut paste change", this._change);
  301. },
  302. tracked: function (el) {
  303. return this.element === $(el)[0];
  304. },
  305. destroy: function (el) {
  306. this._editor().destory();
  307. },
  308. update: function (msg) {
  309. this._editor().setContent(msg.value, {format: 'raw'});
  310. },
  311. init: function (update, msg) {
  312. this.update(update);
  313. },
  314. makeInit: function () {
  315. return {
  316. element: this.element,
  317. tracker: this.trackerName,
  318. value: this.getContent()
  319. };
  320. },
  321. _change: function (e) {
  322. if (inRemoteUpdate) {
  323. return;
  324. }
  325. sendData({
  326. tracker: this.trackerName,
  327. element: this.element,
  328. value: this.getContent()
  329. });
  330. },
  331. _editor: function () {
  332. if (typeof tinymce == "undefined") {
  333. return;
  334. }
  335. return $(this.element).data("tinyEditor");
  336. },
  337. getContent: function () {
  338. return this._editor().getContent();
  339. }
  340. });
  341. tinymceEditor.scan = function () {
  342. //scan all the elements that contain tinyMCE editors
  343. if (typeof tinymce == "undefined") {
  344. return;
  345. }
  346. var result = [];
  347. $(window.tinymce.editors).each(function (i, ed) {
  348. result.push($('#'+ed.id));
  349. //its impossible to retrieve a single editor from a container, so lets store it
  350. $('#'+ed.id).data("tinyEditor", ed);
  351. });
  352. return $(result);
  353. };
  354. tinymceEditor.tracked = function (el) {
  355. if (typeof tinymce == "undefined") {
  356. return false;
  357. }
  358. el = $(el)[0];
  359. return !!$(el).data("tinyEditor");
  360. /*var flag = false;
  361. $(window.tinymce.editors).each(function (i, ed) {
  362. if (el.id == ed.id) {
  363. flag = true;
  364. }
  365. });
  366. return flag;*/
  367. };
  368. TogetherJS.addTracker(tinymceEditor, true);
  369. ///////////////// END OF TINYMCE ///////////////////////////////////
  370. function buildTrackers() {
  371. assert(! liveTrackers.length);
  372. util.forEachAttr(editTrackers, function (TrackerClass) {
  373. var els = TrackerClass.scan();
  374. if (els) {
  375. $.each(els, function () {
  376. var tracker = new TrackerClass(this);
  377. $(this).data("togetherjsHistory", ot.SimpleHistory(session.clientId, tracker.getContent(), 1));
  378. liveTrackers.push(tracker);
  379. });
  380. }
  381. });
  382. }
  383. function destroyTrackers() {
  384. liveTrackers.forEach(function (tracker) {
  385. tracker.destroy();
  386. });
  387. liveTrackers = [];
  388. }
  389. function elementTracked(el) {
  390. var result = false;
  391. util.forEachAttr(editTrackers, function (TrackerClass) {
  392. if (TrackerClass.tracked(el)) {
  393. result = true;
  394. }
  395. });
  396. return result;
  397. }
  398. function getTracker(el, name) {
  399. el = $(el)[0];
  400. for (var i=0; i<liveTrackers.length; i++) {
  401. var tracker = liveTrackers[i];
  402. if (tracker.tracked(el)) {
  403. //FIXME: assert statement below throws an exception when data is submitted to the hub too fast
  404. //in other words, name == tracker.trackerName instead of name == tracker when someone types too fast in the tracked editor
  405. //commenting out this assert statement solves the problem
  406. assert((! name) || name == tracker.trackerName, "Expected to map to a tracker type", name, "but got", tracker.trackerName);
  407. return tracker;
  408. }
  409. }
  410. return null;
  411. }
  412. var TEXT_TYPES = (
  413. "color date datetime datetime-local email " +
  414. "tel text time week").split(/ /g);
  415. function isText(el) {
  416. el = $(el);
  417. var tag = el.prop("tagName");
  418. var type = (el.prop("type") || "text").toLowerCase();
  419. if (tag == "TEXTAREA") {
  420. return true;
  421. }
  422. if (tag == "INPUT" && TEXT_TYPES.indexOf(type) != -1) {
  423. return true;
  424. }
  425. return false;
  426. }
  427. function getValue(el) {
  428. el = $(el);
  429. if (isCheckable(el)) {
  430. return el.prop("checked");
  431. } else {
  432. return el.val();
  433. }
  434. }
  435. function getElementType(el) {
  436. el = $(el)[0];
  437. if (el.tagName == "TEXTAREA") {
  438. return "textarea";
  439. }
  440. if (el.tagName == "SELECT") {
  441. return "select";
  442. }
  443. if (el.tagName == "INPUT") {
  444. return (el.getAttribute("type") || "text").toLowerCase();
  445. }
  446. return "?";
  447. }
  448. function setValue(el, value) {
  449. el = $(el);
  450. var changed = false;
  451. if (isCheckable(el)) {
  452. var checked = !! el.prop("checked");
  453. value = !! value;
  454. if (checked != value) {
  455. changed = true;
  456. el.prop("checked", value);
  457. }
  458. } else {
  459. if (el.val() != value) {
  460. changed = true;
  461. el.val(value);
  462. }
  463. }
  464. if (changed) {
  465. eventMaker.fireChange(el);
  466. }
  467. }
  468. /* Send the top of this history queue, if it hasn't been already sent. */
  469. function maybeSendUpdate(element, history, tracker) {
  470. var change = history.getNextToSend();
  471. if (! change) {
  472. /* nothing to send */
  473. return;
  474. }
  475. var msg = {
  476. type: "form-update",
  477. element: element,
  478. "server-echo": true,
  479. replace: {
  480. id: change.id,
  481. basis: change.basis,
  482. delta: {
  483. start: change.delta.start,
  484. del: change.delta.del,
  485. text: change.delta.text
  486. }
  487. }
  488. };
  489. if (tracker) {
  490. msg.tracker = tracker;
  491. }
  492. session.send(msg);
  493. }
  494. session.hub.on("form-update", function (msg) {
  495. if (! msg.sameUrl) {
  496. return;
  497. }
  498. var el = $(elementFinder.findElement(msg.element));
  499. var tracker;
  500. if (msg.tracker) {
  501. tracker = getTracker(el, msg.tracker);
  502. assert(tracker);
  503. }
  504. var focusedEl = el[0].ownerDocument.activeElement;
  505. var focusedElSelection;
  506. if (isText(focusedEl)) {
  507. focusedElSelection = [focusedEl.selectionStart, focusedEl.selectionEnd];
  508. }
  509. var selection;
  510. if (isText(el)) {
  511. selection = [el[0].selectionStart, el[0].selectionEnd];
  512. }
  513. var value;
  514. if (msg.replace) {
  515. var history = el.data("togetherjsHistory");
  516. if (!history) {
  517. console.warn("form update received for uninitialized form element");
  518. return;
  519. }
  520. history.setSelection(selection);
  521. // make a real TextReplace object.
  522. msg.replace.delta = ot.TextReplace(msg.replace.delta.start,
  523. msg.replace.delta.del,
  524. msg.replace.delta.text);
  525. // apply this change to the history
  526. var changed = history.commit(msg.replace);
  527. var trackerName = null;
  528. if (typeof tracker != "undefined") {
  529. trackerName = tracker.trackerName;
  530. }
  531. maybeSendUpdate(msg.element, history, trackerName);
  532. if (! changed) {
  533. return;
  534. }
  535. value = history.current;
  536. selection = history.getSelection();
  537. } else {
  538. value = msg.value;
  539. }
  540. inRemoteUpdate = true;
  541. try {
  542. if(tracker) {
  543. tracker.update({value:value});
  544. } else {
  545. setValue(el, value);
  546. }
  547. if (isText(el)) {
  548. el[0].selectionStart = selection[0];
  549. el[0].selectionEnd = selection[1];
  550. }
  551. // return focus to original input:
  552. if (focusedEl != el[0]) {
  553. focusedEl.focus();
  554. if (isText(focusedEl)) {
  555. focusedEl.selectionStart = focusedElSelection[0];
  556. focusedEl.selectionEnd = focusedElSelection[1];
  557. }
  558. }
  559. } finally {
  560. inRemoteUpdate = false;
  561. }
  562. });
  563. var initSent = false;
  564. function sendInit() {
  565. initSent = true;
  566. var msg = {
  567. type: "form-init",
  568. pageAge: Date.now() - TogetherJS.pageLoaded,
  569. updates: []
  570. };
  571. var els = $("textarea, input, select");
  572. els.each(function () {
  573. if (elementFinder.ignoreElement(this) || elementTracked(this) ||
  574. suppressSync(this)) {
  575. return;
  576. }
  577. var el = $(this);
  578. var value = getValue(el);
  579. var upd = {
  580. element: elementFinder.elementLocation(this),
  581. //elementType: getElementType(el), // added in 5cbb88c9a but unused
  582. value: value
  583. };
  584. if (isText(el)) {
  585. var history = el.data("togetherjsHistory");
  586. if (history) {
  587. upd.value = history.committed;
  588. upd.basis = history.basis;
  589. }
  590. }
  591. msg.updates.push(upd);
  592. });
  593. liveTrackers.forEach(function (tracker) {
  594. var init = tracker.makeInit();
  595. assert(tracker.tracked(init.element));
  596. var history = $(init.element).data("togetherjsHistory");
  597. if (history) {
  598. init.value = history.committed;
  599. init.basis = history.basis;
  600. }
  601. init.element = elementFinder.elementLocation($(init.element));
  602. msg.updates.push(init);
  603. });
  604. if (msg.updates.length) {
  605. session.send(msg);
  606. }
  607. }
  608. function setInit() {
  609. var els = $("textarea, input, select");
  610. els.each(function () {
  611. if (elementTracked(this)) {
  612. return;
  613. }
  614. if (elementFinder.ignoreElement(this)) {
  615. return;
  616. }
  617. var el = $(this);
  618. var value = getValue(el);
  619. el.data("togetherjsHistory", ot.SimpleHistory(session.clientId, value, 1));
  620. });
  621. destroyTrackers();
  622. buildTrackers();
  623. }
  624. session.on("reinitialize", setInit);
  625. session.on("ui-ready", setInit);
  626. session.on("close", destroyTrackers);
  627. session.hub.on("form-init", function (msg) {
  628. if (! msg.sameUrl) {
  629. return;
  630. }
  631. if (initSent) {
  632. // In a 3+-peer situation more than one client may init; in this case
  633. // we're probably the other peer, and not the peer that needs the init
  634. // A quick check to see if we should init...
  635. var myAge = Date.now() - TogetherJS.pageLoaded;
  636. if (msg.pageAge < myAge) {
  637. // We've been around longer than the other person...
  638. return;
  639. }
  640. }
  641. // FIXME: need to figure out when to ignore inits
  642. msg.updates.forEach(function (update) {
  643. var el;
  644. try {
  645. el = elementFinder.findElement(update.element);
  646. } catch (e) {
  647. /* skip missing element */
  648. console.warn(e);
  649. return;
  650. }
  651. inRemoteUpdate = true;
  652. try {
  653. if (update.tracker) {
  654. var tracker = getTracker(el, update.tracker);
  655. assert(tracker);
  656. tracker.init(update, msg);
  657. } else {
  658. setValue(el, update.value);
  659. }
  660. if (update.basis) {
  661. var history = $(el).data("togetherjsHistory");
  662. // don't overwrite history if we're already up to date
  663. // (we might have outstanding queued changes we don't want to lose)
  664. if (!(history && history.basis === update.basis &&
  665. // if history.basis is 1, the form could have lingering
  666. // edits from before togetherjs was launched. that's too bad,
  667. // we need to erase them to resynchronize with the peer
  668. // we just asked to join.
  669. history.basis !== 1)) {
  670. $(el).data("togetherjsHistory", ot.SimpleHistory(session.clientId, update.value, update.basis));
  671. }
  672. }
  673. } finally {
  674. inRemoteUpdate = false;
  675. }
  676. });
  677. });
  678. var lastFocus = null;
  679. function focus(event) {
  680. var target = event.target;
  681. if (elementFinder.ignoreElement(target) || elementTracked(target)) {
  682. blur(event);
  683. return;
  684. }
  685. if (target != lastFocus) {
  686. lastFocus = target;
  687. session.send({type: "form-focus", element: elementFinder.elementLocation(target)});
  688. }
  689. }
  690. function blur(event) {
  691. var target = event.target;
  692. if (lastFocus) {
  693. lastFocus = null;
  694. session.send({type: "form-focus", element: null});
  695. }
  696. }
  697. var focusElements = {};
  698. session.hub.on("form-focus", function (msg) {
  699. if (! msg.sameUrl) {
  700. return;
  701. }
  702. var current = focusElements[msg.peer.id];
  703. if (current) {
  704. current.remove();
  705. current = null;
  706. }
  707. if (! msg.element) {
  708. // A blur
  709. return;
  710. }
  711. var element = elementFinder.findElement(msg.element);
  712. var el = createFocusElement(msg.peer, element);
  713. if (el) {
  714. focusElements[msg.peer.id] = el;
  715. }
  716. });
  717. function createFocusElement(peer, around) {
  718. around = $(around);
  719. var aroundOffset = around.offset();
  720. if (! aroundOffset) {
  721. console.warn("Could not get offset of element:", around[0]);
  722. return null;
  723. }
  724. var el = templating.sub("focus", {peer: peer});
  725. el = el.find(".togetherjs-focus");
  726. el.css({
  727. top: aroundOffset.top-FOCUS_BUFFER + "px",
  728. left: aroundOffset.left-FOCUS_BUFFER + "px",
  729. width: around.outerWidth() + (FOCUS_BUFFER*2) + "px",
  730. height: around.outerHeight() + (FOCUS_BUFFER*2) + "px"
  731. });
  732. $(document.body).append(el);
  733. return el;
  734. }
  735. session.on("ui-ready", function () {
  736. $(document).on("change", change);
  737. // note that textInput, keydown, and keypress aren't appropriate events
  738. // to watch, since they fire *before* the element's value changes.
  739. $(document).on("input keyup cut paste", maybeChange);
  740. $(document).on("focusin", focus);
  741. $(document).on("focusout", blur);
  742. });
  743. session.on("close", function () {
  744. $(document).off("change", change);
  745. $(document).off("input keyup cut paste", maybeChange);
  746. $(document).off("focusin", focus);
  747. $(document).off("focusout", blur);
  748. });
  749. session.hub.on("hello", function (msg) {
  750. if (msg.sameUrl) {
  751. setTimeout(function () {
  752. sendInit();
  753. if (lastFocus) {
  754. session.send({type: "form-focus", element: elementFinder.elementLocation(lastFocus)});
  755. }
  756. });
  757. }
  758. });
  759. return forms;
  760. });