ui.js 49 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509
  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(["require", "jquery", "util", "session", "templates", "templating", "linkify", "peers", "windowing", "tinycolor", "elementFinder", "visibilityApi"], function (require, $, util, session, templates, templating, linkify, peers, windowing, tinycolor, elementFinder, visibilityApi) {
  5. var ui = util.Module('ui');
  6. var assert = util.assert;
  7. var AssertionError = util.AssertionError;
  8. var chat;
  9. var $window = $(window);
  10. // This is also in togetherjs.less, as @button-height:
  11. var BUTTON_HEIGHT = 60 + 1; // 60 is button height, 1 is border
  12. // chat TextArea
  13. var TEXTAREA_LINE_HEIGHT = 20; // in pixels
  14. var TEXTAREA_MAX_LINES = 5;
  15. // This is also in togetherjs.less, under .togetherjs-animated
  16. var ANIMATION_DURATION = 1000;
  17. // Time the new user window sticks around until it fades away:
  18. var NEW_USER_FADE_TIMEOUT = 5000;
  19. // This is set when an animation will keep the UI from being ready
  20. // (until this time):
  21. var finishedAt = null;
  22. // Time in milliseconds for the dock to animate out:
  23. var DOCK_ANIMATION_TIME = 300;
  24. // If two chat messages come from the same person in this time
  25. // (milliseconds) then they are collapsed into one message:
  26. var COLLAPSE_MESSAGE_LIMIT = 5000;
  27. var COLORS = [
  28. "#8A2BE2", "#7FFF00", "#DC143C", "#00FFFF", "#8FBC8F", "#FF8C00", "#FF00FF",
  29. "#FFD700", "#F08080", "#90EE90", "#FF6347"];
  30. // This would be a circular import, but we just need the chat module sometime
  31. // after everything is loaded, and this is sure to complete by that time:
  32. require(["chat"], function (c) {
  33. chat = c;
  34. });
  35. /* Displays some toggleable element; toggleable elements have a
  36. data-toggles attribute that indicates what other elements should
  37. be hidden when this element is shown. */
  38. ui.displayToggle = function (el) {
  39. el = $(el);
  40. assert(el.length, "No element", arguments[0]);
  41. var other = $(el.attr("data-toggles"));
  42. assert(other.length, "Cannot toggle", el[0], "selector", other.selector);
  43. other.hide();
  44. el.show();
  45. };
  46. function panelPosition() {
  47. var iface = $("#togetherjs-dock");
  48. if (iface.hasClass("togetherjs-dock-right")) {
  49. return "right";
  50. } else if (iface.hasClass("togetherjs-dock-left")) {
  51. return "left";
  52. } else if (iface.hasClass("togetherjs-dock-bottom")) {
  53. return "bottom";
  54. } else {
  55. throw new AssertionError("#togetherjs-dock doesn't have positioning class");
  56. }
  57. }
  58. ui.container = null;
  59. // This is used for some signalling when ui.prepareUI and/or
  60. // ui.activateUI is called before the DOM is fully loaded:
  61. var deferringPrepareUI = null;
  62. function deferForContainer(func) {
  63. /* Defers any calls to func() until after ui.container is set
  64. Function cannot have a return value (as sometimes the call will
  65. become async). Use like:
  66. method: deferForContainer(function (args) {...})
  67. */
  68. return function () {
  69. if (ui.container) {
  70. func.apply(this, arguments);
  71. }
  72. var self = this;
  73. var args = Array.prototype.slice.call(arguments);
  74. session.once("ui-ready", function () {
  75. func.apply(self, args);
  76. });
  77. };
  78. }
  79. // This is called before activateUI; it doesn't bind anything, but does display
  80. // the dock
  81. // FIXME: because this module has lots of requirements we can't do
  82. // this before those requirements are loaded. Maybe worth splitting
  83. // this out? OTOH, in production we should have all the files
  84. // combined so there's not much problem loading those modules.
  85. ui.prepareUI = function () {
  86. if (! (document.readyState == "complete" || document.readyState == "interactive")) {
  87. // Too soon! Wait a sec...
  88. deferringPrepareUI = "deferring";
  89. document.addEventListener("DOMContentLoaded", function () {
  90. var d = deferringPrepareUI;
  91. deferringPrepareUI = null;
  92. ui.prepareUI();
  93. // This happens when ui.activateUI is called before the document has been
  94. // loaded:
  95. if (d == "activate") {
  96. ui.activateUI();
  97. }
  98. });
  99. return;
  100. }
  101. var container = ui.container = $(templates("interface"));
  102. assert(container.length);
  103. $("body").append(container);
  104. fixupAvatars(container);
  105. if (session.firstRun && TogetherJS.startTarget) {
  106. // Time at which the UI will be fully ready:
  107. // (We have to do this because the offset won't be quite right
  108. // until the animation finishes - attempts to calculate the
  109. // offset without taking into account CSS transforms have so far
  110. // failed.)
  111. var timeoutSeconds = DOCK_ANIMATION_TIME / 1000;
  112. finishedAt = Date.now() + DOCK_ANIMATION_TIME + 50;
  113. setTimeout(function () {
  114. finishedAt = Date.now() + DOCK_ANIMATION_TIME + 40;
  115. var iface = container.find("#togetherjs-dock");
  116. var start = iface.offset();
  117. var pos = $(TogetherJS.startTarget).offset();
  118. pos.top = Math.floor(pos.top - start.top);
  119. pos.left = Math.floor(pos.left - start.left);
  120. var translate = "translate(" + pos.left + "px, " + pos.top + "px)";
  121. iface.css({
  122. MozTransform: translate,
  123. WebkitTransform: translate,
  124. transform: translate,
  125. opacity: "0.0"
  126. });
  127. setTimeout(function () {
  128. // We keep recalculating because the setTimeout times aren't always so accurate:
  129. finishedAt = Date.now() + DOCK_ANIMATION_TIME + 20;
  130. var transition = "transform " + timeoutSeconds + "s ease-out, ";
  131. transition += "opacity " + timeoutSeconds + "s ease-out";
  132. iface.css({
  133. opacity: "1.0",
  134. MozTransition: "-moz-" + transition,
  135. MozTransform: "translate(0, 0)",
  136. WebkitTransition: "-webkit-" + transition,
  137. WebkitTransform: "translate(0, 0)",
  138. transition: transition,
  139. transform: "translate(0, 0)"
  140. });
  141. setTimeout(function () {
  142. finishedAt = null;
  143. iface.attr("style", "");
  144. }, 510);
  145. }, 5);
  146. }, 5);
  147. }
  148. if (TogetherJS.startTarget) {
  149. var el = $(TogetherJS.startTarget);
  150. var text = el.text().toLowerCase().replace(/\s+/g, " ");
  151. text = text.replace(/^\s*/, "").replace(/\s*$/, "");
  152. if (text == "start togetherjs") {
  153. el.attr("data-end-togetherjs-html", "End TogetherJS");
  154. }
  155. if (el.attr("data-end-togetherjs-html")) {
  156. el.attr("data-start-togetherjs-html", el.html());
  157. el.html(el.attr("data-end-togetherjs-html"));
  158. }
  159. el.addClass("togetherjs-started");
  160. }
  161. ui.container.find(".togetherjs-window > header, .togetherjs-modal > header").each(function () {
  162. $(this).append($('<button class="togetherjs-close"></button>'));
  163. });
  164. TogetherJS.config.track("disableWebRTC", function (hide, previous) {
  165. if (hide && ! previous) {
  166. ui.container.find("#togetherjs-audio-button").hide();
  167. adjustDockSize(-1);
  168. } else if ((! hide) && previous) {
  169. ui.container.find("#togetherjs-audio-button").show();
  170. adjustDockSize(1);
  171. }
  172. });
  173. };
  174. // After prepareUI, this actually makes the interface live. We have
  175. // to do this later because we call prepareUI when many components
  176. // aren't initialized, so we don't even want the user to be able to
  177. // interact with the interface. But activateUI is called once
  178. // everything is loaded and ready for interaction.
  179. ui.activateUI = function () {
  180. if (deferringPrepareUI) {
  181. console.warn("ui.activateUI called before document is ready; waiting...");
  182. deferringPrepareUI = "activate";
  183. return;
  184. }
  185. if (! ui.container) {
  186. ui.prepareUI();
  187. }
  188. var container = ui.container;
  189. //create the overlay
  190. if($.browser.mobile) {
  191. // $("body").append( "\x3cdiv class='overlay' style='position: absolute; top: 0; left: 0; background-color: rgba(0,0,0,0); width: 120%; height: 100%; z-index: 1000; margin: -10px'>\x3c/div>" );
  192. }
  193. // The share link:
  194. ui.prepareShareLink(container);
  195. container.find("input.togetherjs-share-link").on("keydown", function (event) {
  196. if (event.which == 27) {
  197. windowing.hide("#togetherjs-share");
  198. return false;
  199. }
  200. return undefined;
  201. });
  202. session.on("shareId", updateShareLink);
  203. // The chat input element:
  204. var input = container.find("#togetherjs-chat-input");
  205. input.bind("keydown", function (event) {
  206. if (event.which == 13 && !event.shiftKey) { // Enter without Shift pressed
  207. submitChat();
  208. return false;
  209. }
  210. if (event.which == 27) { // Escape
  211. windowing.hide("#togetherjs-chat");
  212. return false;
  213. }
  214. });
  215. function submitChat() {
  216. var val = input.val();
  217. if ($.trim(val)) {
  218. input.val("");
  219. // triggering the event manually to avoid the addition of newline character to the textarea:
  220. input.trigger("input").trigger("propertychange");
  221. chat.submit(val);
  222. }
  223. }
  224. // auto-resize textarea:
  225. input.on("input propertychange", function () {
  226. var $this = $(this);
  227. var actualHeight = $this.height();
  228. // reset the height of textarea to remove trailing empty space (used for shrinking):
  229. $this.height(TEXTAREA_LINE_HEIGHT);
  230. this.scrollTop = 0;
  231. // scroll to bottom:
  232. this.scrollTop = 9999;
  233. var newHeight = this.scrollTop + $this.height();
  234. var maxHeight = TEXTAREA_MAX_LINES * TEXTAREA_LINE_HEIGHT;
  235. if (newHeight > maxHeight) {
  236. newHeight = maxHeight;
  237. this.style.overflowY = "scroll";
  238. } else {
  239. this.style.overflowY = "hidden";
  240. }
  241. this.style.height = newHeight + "px";
  242. var diff = newHeight - actualHeight;
  243. $("#togetherjs-chat-input-box").height($("#togetherjs-chat-input-box").height() + diff);
  244. $("#togetherjs-chat-messages").height($("#togetherjs-chat-messages").height() - diff);
  245. return false;
  246. });
  247. util.testExpose({submitChat: submitChat});
  248. // Moving the window:
  249. // FIXME: this should probably be stickier, and not just move the window around
  250. // so abruptly
  251. var anchor = container.find("#togetherjs-dock-anchor");
  252. assert(anchor.length);
  253. // FIXME: This is in place to temporarily disable dock dragging:
  254. anchor = container.find("#togetherjs-dock-anchor-disabled");
  255. anchor.mousedown(function (event) {
  256. var iface = $("#togetherjs-dock");
  257. // FIXME: switch to .offset() and pageX/Y
  258. var startPos = panelPosition();
  259. function selectoff() {
  260. return false;
  261. }
  262. function mousemove(event2) {
  263. var fromRight = $window.width() + window.pageXOffset - event2.pageX;
  264. var fromLeft = event2.pageX - window.pageXOffset;
  265. var fromBottom = $window.height() + window.pageYOffset - event2.pageY;
  266. // FIXME: this is to temporarily disable the bottom view:
  267. fromBottom = 10000;
  268. var pos;
  269. if (fromLeft < fromRight && fromLeft < fromBottom) {
  270. pos = "left";
  271. } else if (fromRight < fromLeft && fromRight < fromBottom) {
  272. pos = "right";
  273. } else {
  274. pos = "bottom";
  275. }
  276. iface.removeClass("togetherjs-dock-left");
  277. iface.removeClass("togetherjs-dock-right");
  278. iface.removeClass("togetherjs-dock-bottom");
  279. iface.addClass("togetherjs-dock-" + pos);
  280. if (startPos && pos != startPos) {
  281. windowing.hide();
  282. startPos = null;
  283. }
  284. }
  285. $(document).bind("mousemove", mousemove);
  286. // If you don't turn selection off it will still select text, and show a
  287. // text selection cursor:
  288. $(document).bind("selectstart", selectoff);
  289. // FIXME: it seems like sometimes we lose the mouseup event, and it's as though
  290. // the mouse is stuck down:
  291. $(document).one("mouseup", function () {
  292. $(document).unbind("mousemove", mousemove);
  293. $(document).unbind("selectstart", selectoff);
  294. });
  295. return false;
  296. });
  297. function openDock() {
  298. $('.togetherjs-window').animate({
  299. opacity: 1
  300. });
  301. $('#togetherjs-dock-participants').animate({
  302. opacity: 1
  303. });
  304. $('#togetherjs-dock #togetherjs-buttons').animate({
  305. opacity: 1
  306. });
  307. //for iphone
  308. if($(window).width() < 480) {
  309. $('.togetherjs-dock-right').animate({
  310. width: "204px"
  311. }, {
  312. duration:60, easing:"linear"
  313. });
  314. }
  315. //for ipad
  316. else {
  317. $('.togetherjs-dock-right').animate({
  318. width: "27%"
  319. }, {
  320. duration:60, easing:"linear"
  321. });
  322. }
  323. // add bg overlay
  324. // $("body").append( "\x3cdiv class='overlay' style='position: absolute; top: 0; left: -2px; background-color: rgba(0,0,0,0.5); width: 200%; height: 400%; z-index: 1000; margin: 0px;'>\x3c/div>" );
  325. //disable vertical scrolling
  326. // $("body").css({
  327. // "position": "fixed",
  328. // top: 0,
  329. // left: 0
  330. // });
  331. //replace the anchor icon
  332. var src = "/togetherjs/images/togetherjs-logo-close.png";
  333. $("#togetherjs-dock-anchor #togetherjs-dock-anchor-horizontal img").attr("src", src);
  334. }
  335. function closeDock() {
  336. //enable vertical scrolling
  337. $("body").css({
  338. "position": "",
  339. top: "",
  340. left: ""
  341. });
  342. //replace the anchor icon
  343. var src = "/togetherjs/images/togetherjs-logo-open.png";
  344. $("#togetherjs-dock-anchor #togetherjs-dock-anchor-horizontal img").attr("src", src);
  345. $('.togetherjs-window').animate({
  346. opacity: 0
  347. });
  348. $('#togetherjs-dock-participants').animate({
  349. opacity: 0
  350. });
  351. $('#togetherjs-dock #togetherjs-buttons').animate({
  352. opacity: 0
  353. });
  354. $('.togetherjs-dock-right').animate({
  355. width: "40px"
  356. }, {
  357. duration:60, easing:"linear"
  358. });
  359. // remove bg overlay
  360. //$(".overlay").remove();
  361. }
  362. // Setting the anchor button + dock mobile actions
  363. if($.browser.mobile) {
  364. // toggle the audio button
  365. $("#togetherjs-audio-button").click(function () {
  366. windowing.toggle("#togetherjs-rtc-not-supported");
  367. });
  368. // toggle the profile button
  369. $("#togetherjs-profile-button").click(function () {
  370. windowing.toggle("#togetherjs-menu-window");
  371. });
  372. // $("body").append( "\x3cdiv class='overlay' style='position: absolute; top: 0; left: -2px; background-color: rgba(0,0,0,0.5); width: 200%; height: 400%; z-index: 1000; margin: 0px'>\x3c/div>" );
  373. //disable vertical scrolling
  374. // $("body").css({
  375. // "position": "fixed",
  376. // top: 0,
  377. // left: 0
  378. // });
  379. //replace the anchor icon
  380. var src = "/togetherjs/images/togetherjs-logo-close.png";
  381. $("#togetherjs-dock-anchor #togetherjs-dock-anchor-horizontal img").attr("src", src);
  382. $("#togetherjs-dock-anchor").toggle(function() {
  383. closeDock();
  384. },function(){
  385. openDock();
  386. });
  387. }
  388. $("#togetherjs-share-button").click(function () {
  389. windowing.toggle("#togetherjs-share");
  390. });
  391. $("#togetherjs-profile-button").click(function (event) {
  392. if ($.browser.mobile) {
  393. windowing.show("#togetherjs-menu-window");
  394. return false;
  395. }
  396. toggleMenu();
  397. event.stopPropagation();
  398. return false;
  399. });
  400. $("#togetherjs-menu-feedback, #togetherjs-menu-feedback-button").click(function(){
  401. windowing.hide();
  402. hideMenu();
  403. windowing.show("#togetherjs-feedback-form");
  404. });
  405. $("#togetherjs-menu-help, #togetherjs-menu-help-button").click(function () {
  406. windowing.hide();
  407. hideMenu();
  408. require(["walkthrough"], function (walkthrough) {
  409. windowing.hide();
  410. walkthrough.start(false);
  411. });
  412. });
  413. $("#togetherjs-menu-update-name").click(function () {
  414. var input = $("#togetherjs-menu .togetherjs-self-name");
  415. input.css({
  416. width: $("#togetherjs-menu").width() - 32 + "px"
  417. });
  418. ui.displayToggle("#togetherjs-menu .togetherjs-self-name");
  419. $("#togetherjs-menu .togetherjs-self-name").focus();
  420. });
  421. $("#togetherjs-menu-update-name-button").click(function () {
  422. windowing.show("#togetherjs-edit-name-window");
  423. $("#togetherjs-edit-name-window input").focus();
  424. });
  425. $("#togetherjs-menu .togetherjs-self-name").bind("keyup change", function (event) {
  426. console.log("alrighty", event);
  427. if (event.which == 13) {
  428. ui.displayToggle("#togetherjs-self-name-display");
  429. return;
  430. }
  431. var val = $("#togetherjs-menu .togetherjs-self-name").val();
  432. console.log("values!!", val);
  433. if (val) {
  434. peers.Self.update({name: val});
  435. }
  436. });
  437. $("#togetherjs-menu-update-avatar, #togetherjs-menu-update-avatar-button").click(function () {
  438. hideMenu();
  439. windowing.show("#togetherjs-avatar-edit");
  440. });
  441. $("#togetherjs-menu-end, #togetherjs-menu-end-button").click(function () {
  442. hideMenu();
  443. windowing.show("#togetherjs-confirm-end");
  444. });
  445. $("#togetherjs-end-session").click(function () {
  446. session.close();
  447. //$(".overlay").remove();
  448. });
  449. $("#togetherjs-menu-update-color").click(function () {
  450. var picker = $("#togetherjs-pick-color");
  451. if (picker.is(":visible")) {
  452. picker.hide();
  453. return;
  454. }
  455. picker.show();
  456. bindPicker();
  457. picker.find(".togetherjs-swatch-active").removeClass("togetherjs-swatch-active");
  458. picker.find(".togetherjs-swatch[data-color=\"" + peers.Self.color + "\"]").addClass("togetherjs-swatch-active");
  459. });
  460. $("#togetherjs-pick-color").click(".togetherjs-swatch", function (event) {
  461. var swatch = $(event.target);
  462. var color = swatch.attr("data-color");
  463. peers.Self.update({
  464. color: color
  465. });
  466. event.stopPropagation();
  467. return false;
  468. });
  469. $("#togetherjs-pick-color").click(function (event) {
  470. $("#togetherjs-pick-color").hide();
  471. event.stopPropagation();
  472. return false;
  473. });
  474. COLORS.forEach(function (color) {
  475. var el = templating.sub("swatch");
  476. el.attr("data-color", color);
  477. var darkened = tinycolor.darken(color);
  478. el.css({
  479. backgroundColor: color,
  480. borderColor: darkened
  481. });
  482. $("#togetherjs-pick-color").append(el);
  483. });
  484. $("#togetherjs-chat-button").click(function () {
  485. windowing.toggle("#togetherjs-chat");
  486. });
  487. session.on("display-window", function (id, element) {
  488. if (id == "togetherjs-chat") {
  489. if (! $.browser.mobile) {
  490. $("#togetherjs-chat-input").focus();
  491. }
  492. } else if (id == "togetherjs-share") {
  493. var link = element.find("input.togetherjs-share-link");
  494. if (link.is(":visible")) {
  495. link.focus().select();
  496. }
  497. }
  498. });
  499. container.find("#togetherjs-chat-notifier").click(function (event) {
  500. if ($(event.target).is("a") || container.is(".togetherjs-close")) {
  501. return;
  502. }
  503. windowing.show("#togetherjs-chat");
  504. });
  505. // FIXME: Don't think this makes sense
  506. $(".togetherjs header.togetherjs-title").each(function (index, item) {
  507. var button = $('<button class="togetherjs-minimize"></button>');
  508. button.click(function (event) {
  509. var window = button.closest(".togetherjs-window");
  510. windowing.hide(window);
  511. });
  512. $(item).append(button);
  513. });
  514. $("#togetherjs-avatar-done").click(function () {
  515. ui.displayToggle("#togetherjs-no-avatar-edit");
  516. });
  517. $("#togetherjs-self-color").css({backgroundColor: peers.Self.color});
  518. var avatar = peers.Self.avatar;
  519. if (avatar) {
  520. $("#togetherjs-self-avatar").attr("src", avatar);
  521. }
  522. var starterButton = $("#togetherjs-starter button");
  523. starterButton.click(function () {
  524. windowing.show("#togetherjs-about");
  525. }).addClass("togetherjs-running");
  526. if (starterButton.text() == "Start TogetherJS") {
  527. starterButton.attr("data-start-text", starterButton.text());
  528. starterButton.text("End TogetherJS Session");
  529. }
  530. ui.activateAvatarEdit(container, {
  531. onSave: function () {
  532. windowing.hide("#togetherjs-avatar-edit");
  533. }
  534. });
  535. TogetherJS.config.track("inviteFromRoom", function (inviter, previous) {
  536. if (inviter) {
  537. container.find("#togetherjs-invite").show();
  538. } else {
  539. container.find("#togetherjs-invite").hide();
  540. }
  541. });
  542. container.find("#togetherjs-menu-refresh-invite").click(refreshInvite);
  543. container.find("#togetherjs-menu-invite-anyone").click(function () {
  544. invite(null);
  545. });
  546. // The following lines should be at the end of this function
  547. // (new code goes above)
  548. session.emit("new-element", ui.container);
  549. if (finishedAt && finishedAt > Date.now()) {
  550. setTimeout(function () {
  551. finishedAt = null;
  552. session.emit("ui-ready", ui);
  553. }, finishedAt - Date.now());
  554. } else {
  555. session.emit("ui-ready", ui);
  556. }
  557. }; // End ui.activateUI()
  558. ui.activateAvatarEdit = function (container, options) {
  559. options = options || {};
  560. var pendingImage = null;
  561. container.find(".togetherjs-avatar-save").prop("disabled", true);
  562. container.find(".togetherjs-avatar-save").click(function () {
  563. if (pendingImage) {
  564. peers.Self.update({avatar: pendingImage});
  565. container.find(".togetherjs-avatar-save").prop("disabled", true);
  566. if (options.onSave) {
  567. options.onSave();
  568. }
  569. }
  570. });
  571. container.find(".togetherjs-upload-avatar").on("change", function () {
  572. util.readFileImage(this).then(function (url) {
  573. sizeDownImage(url).then(function (smallUrl) {
  574. pendingImage = smallUrl;
  575. container.find(".togetherjs-avatar-preview").css({
  576. backgroundImage: 'url(' + pendingImage + ')'
  577. });
  578. container.find(".togetherjs-avatar-save").prop("disabled", false);
  579. if (options.onPending) {
  580. options.onPending();
  581. }
  582. });
  583. });
  584. });
  585. };
  586. function sizeDownImage(imageUrl) {
  587. return util.Deferred(function (def) {
  588. var $canvas = $("<canvas>");
  589. $canvas[0].height = session.AVATAR_SIZE;
  590. $canvas[0].width = session.AVATAR_SIZE;
  591. var context = $canvas[0].getContext("2d");
  592. var img = new Image();
  593. img.src = imageUrl;
  594. // Sometimes the DOM updates immediately to call
  595. // naturalWidth/etc, and sometimes it doesn't; using setTimeout
  596. // gives it a chance to catch up
  597. setTimeout(function () {
  598. var width = img.naturalWidth || img.width;
  599. var height = img.naturalHeight || img.height;
  600. width = width * (session.AVATAR_SIZE / height);
  601. height = session.AVATAR_SIZE;
  602. context.drawImage(img, 0, 0, width, height);
  603. def.resolve($canvas[0].toDataURL("image/png"));
  604. });
  605. });
  606. }
  607. function fixupAvatars(container) {
  608. /* All <div class="togetherjs-person" /> elements need an element inside,
  609. so we add that element here */
  610. container.find(".togetherjs-person").each(function () {
  611. var $this = $(this);
  612. var inner = $this.find(".togetherjs-person-avatar-swatch");
  613. if (! inner.length) {
  614. $this.append('<div class="togetherjs-person-avatar-swatch"></div>');
  615. }
  616. });
  617. }
  618. ui.prepareShareLink = function (container) {
  619. container.find("input.togetherjs-share-link").click(function () {
  620. $(this).select();
  621. }).change(function () {
  622. updateShareLink();
  623. });
  624. container.find("a.togetherjs-share-link").click(function () {
  625. // FIXME: this is currently opening up Bluetooth, not sharing a link
  626. if (false && window.MozActivity) {
  627. var activity = new MozActivity({
  628. name: "share",
  629. data: {
  630. type: "url",
  631. url: $(this).attr("href")
  632. }
  633. });
  634. }
  635. // FIXME: should show some help if you actually try to follow the link
  636. // like this, instead of simply suppressing it
  637. return false;
  638. });
  639. updateShareLink();
  640. };
  641. // Menu
  642. function showMenu(event) {
  643. var el = $("#togetherjs-menu");
  644. assert(el.length);
  645. el.show();
  646. bindMenu();
  647. $(document).bind("click", maybeHideMenu);
  648. }
  649. function bindMenu() {
  650. var el = $("#togetherjs-menu:visible");
  651. if (el.length) {
  652. var bound = $("#togetherjs-profile-button");
  653. var boundOffset = bound.offset();
  654. el.css({
  655. top: boundOffset.top + bound.height() - $window.scrollTop() + "px",
  656. left: (boundOffset.left + bound.width() - 10 - el.width() - $window.scrollLeft()) + "px"
  657. });
  658. }
  659. }
  660. function bindPicker() {
  661. var picker = $("#togetherjs-pick-color:visible");
  662. if (picker.length) {
  663. var menu = $("#togetherjs-menu-update-color");
  664. var menuOffset = menu.offset();
  665. picker.css({
  666. top: menuOffset.top + menu.height(),
  667. left: menuOffset.left
  668. });
  669. }
  670. }
  671. session.on("resize", function () {
  672. bindMenu();
  673. bindPicker();
  674. });
  675. function toggleMenu() {
  676. if ($("#togetherjs-menu").is(":visible")) {
  677. hideMenu();
  678. } else {
  679. showMenu();
  680. }
  681. }
  682. function hideMenu() {
  683. var el = $("#togetherjs-menu");
  684. el.hide();
  685. $(document).unbind("click", maybeHideMenu);
  686. ui.displayToggle("#togetherjs-self-name-display");
  687. $("#togetherjs-pick-color").hide();
  688. }
  689. function maybeHideMenu(event) {
  690. var t = event.target;
  691. while (t) {
  692. if (t.id == "togetherjs-menu") {
  693. // Click inside the menu, ignore this
  694. return;
  695. }
  696. t = t.parentNode;
  697. }
  698. hideMenu();
  699. }
  700. function adjustDockSize(buttons) {
  701. /* Add or remove spots from the dock; positive number to
  702. add button(s), negative number to remove button(s)
  703. */
  704. assert(typeof buttons == "number");
  705. assert(buttons && Math.floor(buttons) == buttons);
  706. var iface = $("#togetherjs-dock");
  707. var newHeight = iface.height() + (BUTTON_HEIGHT * buttons);
  708. assert(newHeight >= BUTTON_HEIGHT * 3, "Height went too low (", newHeight,
  709. "), should never be less than 3 buttons high (", BUTTON_HEIGHT * 3, ")");
  710. iface.css({
  711. height: newHeight + "px"
  712. });
  713. }
  714. // Misc
  715. function updateShareLink() {
  716. var input = $("input.togetherjs-share-link");
  717. var link = $("a.togetherjs-share-link");
  718. var display = $("#togetherjs-session-id");
  719. if (! session.shareId) {
  720. input.val("");
  721. link.attr("href", "#");
  722. display.text("(none)");
  723. } else {
  724. input.val(session.shareUrl());
  725. link.attr("href", session.shareUrl());
  726. display.text(session.shareId);
  727. }
  728. }
  729. session.on("close", function () {
  730. if($.browser.mobile) {
  731. // remove bg overlay
  732. //$(".overlay").remove();
  733. //after hitting End, reset window draggin
  734. $("body").css({
  735. "position": "",
  736. top: "",
  737. left: ""
  738. });
  739. }
  740. if (ui.container) {
  741. ui.container.remove();
  742. ui.container = null;
  743. }
  744. // Clear out any other spurious elements:
  745. $(".togetherjs").remove();
  746. var starterButton = $("#togetherjs-starter button");
  747. starterButton.removeClass("togetherjs-running");
  748. if (starterButton.attr("data-start-text")) {
  749. starterButton.text(starterButton.attr("data-start-text"));
  750. starterButton.attr("data-start-text", "");
  751. }
  752. if (TogetherJS.startTarget) {
  753. var el = $(TogetherJS.startTarget);
  754. if (el.attr("data-start-togetherjs-html")) {
  755. el.html(el.attr("data-start-togetherjs-html"));
  756. }
  757. el.removeClass("togetherjs-started");
  758. }
  759. });
  760. ui.chat = {
  761. text: function (attrs) {
  762. assert(typeof attrs.text == "string");
  763. assert(attrs.peer);
  764. assert(attrs.messageId);
  765. var date = attrs.date || Date.now();
  766. var lastEl = ui.container.find("#togetherjs-chat .togetherjs-chat-message");
  767. if (lastEl.length) {
  768. lastEl = $(lastEl[lastEl.length-1]);
  769. }
  770. var lastDate = null;
  771. if (lastEl) {
  772. lastDate = parseInt(lastEl.attr("data-date"), 10);
  773. }
  774. if (lastEl && lastEl.attr("data-person") == attrs.peer.id &&
  775. lastDate && date < lastDate + COLLAPSE_MESSAGE_LIMIT) {
  776. lastEl.attr("data-date", date);
  777. var content = lastEl.find(".togetherjs-chat-content");
  778. assert(content.length);
  779. attrs.text = content.text() + "\n" + attrs.text;
  780. attrs.messageId = lastEl.attr("data-message-id");
  781. lastEl.remove();
  782. }
  783. var el = templating.sub("chat-message", {
  784. peer: attrs.peer,
  785. content: attrs.text,
  786. date: date
  787. });
  788. linkify(el.find(".togetherjs-chat-content"));
  789. el.attr("data-person", attrs.peer.id)
  790. .attr("data-date", date)
  791. .attr("data-message-id", attrs.messageId);
  792. ui.chat.add(el, attrs.messageId, attrs.notify);
  793. },
  794. joinedSession: function (attrs) {
  795. assert(attrs.peer);
  796. var date = attrs.date || Date.now();
  797. var el = templating.sub("chat-joined", {
  798. peer: attrs.peer,
  799. date: date
  800. });
  801. // FIXME: should bind the notification to the dock location
  802. ui.chat.add(el, attrs.peer.className("join-message-"), 4000);
  803. },
  804. leftSession: function (attrs) {
  805. assert(attrs.peer);
  806. var date = attrs.date || Date.now();
  807. var el = templating.sub("chat-left", {
  808. peer: attrs.peer,
  809. date: date,
  810. declinedJoin: attrs.declinedJoin
  811. });
  812. // FIXME: should bind the notification to the dock location
  813. ui.chat.add(el, attrs.peer.className("join-message-"), 4000);
  814. },
  815. system: function (attrs) {
  816. assert(! attrs.peer);
  817. assert(typeof attrs.text == "string");
  818. var date = attrs.date || Date.now();
  819. var el = templating.sub("chat-system", {
  820. content: attrs.text,
  821. date: date
  822. });
  823. ui.chat.add(el, undefined, true);
  824. },
  825. clear: deferForContainer(function () {
  826. var container = ui.container.find("#togetherjs-chat-messages");
  827. container.empty();
  828. }),
  829. urlChange: function (attrs) {
  830. assert(attrs.peer);
  831. assert(typeof attrs.url == "string");
  832. assert(typeof attrs.sameUrl == "boolean");
  833. var messageId = attrs.peer.className("url-change-");
  834. // FIXME: duplicating functionality in .add():
  835. var realId = "togetherjs-chat-" + messageId;
  836. var date = attrs.date || Date.now();
  837. var title;
  838. // FIXME: strip off common domain from msg.url? E.g., if I'm on
  839. // http://example.com/foobar, and someone goes to http://example.com/baz then
  840. // show only /baz
  841. // FIXME: truncate long titles
  842. if (attrs.title) {
  843. title = attrs.title + " (" + attrs.url + ")";
  844. } else {
  845. title = attrs.url;
  846. }
  847. var el = templating.sub("url-change", {
  848. peer: attrs.peer,
  849. date: date,
  850. href: attrs.url,
  851. title: title,
  852. sameUrl: attrs.sameUrl
  853. });
  854. el.find(".togetherjs-nudge").click(function () {
  855. attrs.peer.nudge();
  856. return false;
  857. });
  858. el.find(".togetherjs-follow").click(function () {
  859. var url = attrs.peer.url;
  860. if (attrs.peer.urlHash) {
  861. url += attrs.peer.urlHash;
  862. }
  863. location.href = url;
  864. });
  865. var notify = ! attrs.sameUrl;
  866. if (attrs.sameUrl && ! $("#" + realId).length) {
  867. // Don't bother showing a same-url notification, if no previous notification
  868. // had been shown
  869. return;
  870. }
  871. ui.chat.add(el, messageId, notify);
  872. },
  873. invite: function (attrs) {
  874. assert(attrs.peer);
  875. assert(typeof attrs.url == "string");
  876. var messageId = attrs.peer.className("invite-");
  877. var date = attrs.date || Date.now();
  878. var hrefTitle = attrs.url.replace(/\#?&togetherjs=.*/, "").replace(/^\w+:\/\//, "");
  879. var el = templating.sub("invite", {
  880. peer: attrs.peer,
  881. date: date,
  882. href: attrs.url,
  883. hrefTitle: hrefTitle,
  884. forEveryone: attrs.forEveryone
  885. });
  886. if (attrs.forEveryone) {
  887. el.find("a").click(function () {
  888. // FIXME: hacky way to do this:
  889. chat.submit("Followed link to " + attrs.url);
  890. });
  891. }
  892. ui.chat.add(el, messageId, true);
  893. },
  894. hideTimeout: null,
  895. add: deferForContainer(function (el, id, notify) {
  896. if (id) {
  897. el.attr("id", "togetherjs-chat-" + util.safeClassName(id));
  898. }
  899. var container = ui.container.find("#togetherjs-chat-messages");
  900. assert(container.length);
  901. var popup = ui.container.find("#togetherjs-chat-notifier");
  902. container.append(el);
  903. ui.chat.scroll();
  904. var doNotify = !! notify;
  905. var section = popup.find("#togetherjs-chat-notifier-message");
  906. if (notify && visibilityApi.hidden()) {
  907. ui.container.find("#togetherjs-notification")[0].play();
  908. }
  909. if (id && section.data("message-id") == id) {
  910. doNotify = true;
  911. }
  912. if (container.is(":visible")) {
  913. doNotify = false;
  914. }
  915. if (doNotify) {
  916. section.empty();
  917. section.append(el.clone(true, true));
  918. if (section.data("message-id") != id) {
  919. section.data("message-id", id || "");
  920. windowing.show(popup);
  921. } else if (! popup.is(":visible")) {
  922. windowing.show(popup);
  923. }
  924. if (typeof notify == "number") {
  925. // This is the amount of time we're supposed to notify
  926. if (this.hideTimeout) {
  927. clearTimeout(this.hideTimeout);
  928. this.hideTimeout = null;
  929. }
  930. this.hideTimeout = setTimeout((function () {
  931. windowing.hide(popup);
  932. this.hideTimeout = null;
  933. }).bind(this), notify);
  934. }
  935. }
  936. }),
  937. scroll: deferForContainer(function () {
  938. var container = ui.container.find("#togetherjs-chat-messages")[0];
  939. container.scrollTop = container.scrollHeight;
  940. })
  941. };
  942. session.on("display-window", function (id, win) {
  943. if (id == "togetherjs-chat") {
  944. ui.chat.scroll();
  945. windowing.hide("#togetherjs-chat-notifier");
  946. }
  947. });
  948. /* This class is bound to peers.Peer instances as peer.view.
  949. The .update() method is regularly called by peer objects when info changes. */
  950. ui.PeerView = util.Class({
  951. constructor: function (peer) {
  952. assert(peer.isSelf !== undefined, "PeerView instantiated with non-Peer object");
  953. this.peer = peer;
  954. this.dockClick = this.dockClick.bind(this);
  955. },
  956. /* Takes an element and sets any person-related attributes on the element
  957. Different from updates, which use the class names we set here: */
  958. setElement: function (el) {
  959. var count = 0;
  960. var classes = ["togetherjs-person", "togetherjs-person-status",
  961. "togetherjs-person-name", "togetherjs-person-name-abbrev",
  962. "togetherjs-person-bgcolor", "togetherjs-person-swatch",
  963. "togetherjs-person-status", "togetherjs-person-role",
  964. "togetherjs-person-url", "togetherjs-person-url-title",
  965. "togetherjs-person-bordercolor"];
  966. classes.forEach(function (cls) {
  967. var els = el.find("." + cls);
  968. els.addClass(this.peer.className(cls + "-"));
  969. count += els.length;
  970. }, this);
  971. if (! count) {
  972. console.warn("setElement(", el, ") doesn't contain any person items");
  973. }
  974. this.updateDisplay(el);
  975. },
  976. updateDisplay: deferForContainer(function (container) {
  977. container = container || ui.container;
  978. var abbrev = this.peer.name;
  979. if (this.peer.isSelf) {
  980. abbrev = "me";
  981. }
  982. container.find("." + this.peer.className("togetherjs-person-name-")).text(this.peer.name || "");
  983. container.find("." + this.peer.className("togetherjs-person-name-abbrev-")).text(abbrev);
  984. var avatarEl = container.find("." + this.peer.className("togetherjs-person-"));
  985. if (this.peer.avatar) {
  986. util.assertValidUrl(this.peer.avatar);
  987. avatarEl.css({
  988. backgroundImage: "url(" + this.peer.avatar + ")"
  989. });
  990. }
  991. if (this.peer.idle == "inactive") {
  992. avatarEl.addClass("togetherjs-person-inactive");
  993. } else {
  994. avatarEl.removeClass("togetherjs-person-inactive");
  995. }
  996. avatarEl.attr("title", this.peer.name);
  997. if (this.peer.color) {
  998. avatarEl.css({
  999. borderColor: this.peer.color
  1000. });
  1001. avatarEl.find(".togetherjs-person-avatar-swatch").css({
  1002. borderTopColor: this.peer.color,
  1003. borderRightColor: this.peer.color
  1004. });
  1005. }
  1006. if (this.peer.color) {
  1007. var colors = container.find("." + this.peer.className("togetherjs-person-bgcolor-"));
  1008. colors.css({
  1009. backgroundColor: this.peer.color
  1010. });
  1011. colors = container.find("." + this.peer.className("togetherjs-person-bordercolor-"));
  1012. colors.css({
  1013. borderColor: this.peer.color
  1014. });
  1015. }
  1016. container.find("." + this.peer.className("togetherjs-person-role-"))
  1017. .text(this.peer.isCreator ? "Creator" : "Participant");
  1018. var urlName = this.peer.title || "";
  1019. if (this.peer.title) {
  1020. urlName += " (";
  1021. }
  1022. urlName += util.truncateCommonDomain(this.peer.url, location.href);
  1023. if (this.peer.title) {
  1024. urlName += ")";
  1025. }
  1026. container.find("." + this.peer.className("togetherjs-person-url-title-"))
  1027. .text(urlName);
  1028. var url = this.peer.url;
  1029. if (this.peer.urlHash) {
  1030. url += this.peer.urlHash;
  1031. }
  1032. container.find("." + this.peer.className("togetherjs-person-url-"))
  1033. .attr("href", url);
  1034. // FIXME: should have richer status:
  1035. container.find("." + this.peer.className("togetherjs-person-status-"))
  1036. .text(this.peer.idle == "active" ? "Active" : "Inactive");
  1037. if (this.peer.isSelf) {
  1038. // FIXME: these could also have consistent/reliable class names:
  1039. var selfName = $(".togetherjs-self-name");
  1040. selfName.each((function (index, el) {
  1041. el = $(el);
  1042. if (el.val() != this.peer.name) {
  1043. el.val(this.peer.name);
  1044. }
  1045. }).bind(this));
  1046. $("#togetherjs-menu-avatar").attr("src", this.peer.avatar);
  1047. if (! this.peer.name) {
  1048. $("#togetherjs-menu .togetherjs-person-name-self").text(this.peer.defaultName);
  1049. }
  1050. }
  1051. if (this.peer.url != session.currentUrl()) {
  1052. container.find("." + this.peer.className("togetherjs-person-"))
  1053. .addClass("togetherjs-person-other-url");
  1054. } else {
  1055. container.find("." + this.peer.className("togetherjs-person-"))
  1056. .removeClass("togetherjs-person-other-url");
  1057. }
  1058. if (this.peer.following) {
  1059. if (this.followCheckbox) {
  1060. this.followCheckbox.prop("checked", true);
  1061. }
  1062. } else {
  1063. if (this.followCheckbox) {
  1064. this.followCheckbox.prop("checked", false);
  1065. }
  1066. }
  1067. // FIXME: add some style based on following?
  1068. updateChatParticipantList();
  1069. this.updateFollow();
  1070. }),
  1071. update: function () {
  1072. if (! this.peer.isSelf) {
  1073. if (this.peer.status == "live") {
  1074. this.dock();
  1075. } else {
  1076. this.undock();
  1077. }
  1078. }
  1079. this.updateDisplay();
  1080. this.updateUrlDisplay();
  1081. },
  1082. updateUrlDisplay: function (force) {
  1083. var url = this.peer.url;
  1084. if ((! url) || (url == this._lastUpdateUrlDisplay && ! force)) {
  1085. return;
  1086. }
  1087. this._lastUpdateUrlDisplay = url;
  1088. var sameUrl = url == session.currentUrl();
  1089. ui.chat.urlChange({
  1090. peer: this.peer,
  1091. url: this.peer.url,
  1092. title: this.peer.title,
  1093. sameUrl: sameUrl
  1094. });
  1095. },
  1096. urlNudge: function () {
  1097. // FIXME: do something more distinct here
  1098. this.updateUrlDisplay(true);
  1099. },
  1100. notifyJoined: function () {
  1101. ui.chat.joinedSession({
  1102. peer: this.peer
  1103. });
  1104. },
  1105. // when there are too many participants in the dock, consolidate the participants to one avatar, and on mouseOver, the dock expands down to reveal the rest of the participants
  1106. // if there are X users in the session
  1107. // then hide the users in the dock
  1108. // and shrink the size of the dock
  1109. // and if you rollover the dock, it expands and reveals the rest of the participants in the dock
  1110. //if users hit X then show the participant button with the consol
  1111. dock: deferForContainer(function () {
  1112. var numberOfUsers = peers.getAllPeers().length;
  1113. // collapse the Dock if too many users
  1114. function CollapsedDock() {
  1115. // decrease/reset dock height
  1116. $("#togetherjs-dock").css("height", 260);
  1117. //replace participant button
  1118. $("#togetherjs-dock-participants").replaceWith("<button id='togetherjs-participantlist-button' class='togetherjs-button'><div class='togetherjs-tooltip togetherjs-dock-person-tooltip'><span class='togetherjs-person-name'>Participants</span><span class='togetherjs-person-tooltip-arrow-r'></span></div><div class='togetherjs-person togetherjs-person-status-overlay' title='Participant List' style='background-image: url("+TogetherJS.baseUrl+"/togetherjs/images/robot-avatar.png); border-color: rgb(255, 0, 0);'></div></button>");
  1119. // new full participant window created on toggle
  1120. $("#togetherjs-participantlist-button").click(function () {
  1121. windowing.toggle("#togetherjs-participantlist");
  1122. });
  1123. }
  1124. // FIXME: turned off for now
  1125. if( numberOfUsers >= 5 && false) {
  1126. CollapsedDock();
  1127. } else {
  1128. // reset
  1129. }
  1130. if (this.dockElement) {
  1131. return;
  1132. }
  1133. this.dockElement = templating.sub("dock-person", {
  1134. peer: this.peer
  1135. });
  1136. this.dockElement.attr("id", this.peer.className("togetherjs-dock-element-"));
  1137. ui.container.find("#togetherjs-dock-participants").append(this.dockElement);
  1138. this.dockElement.find(".togetherjs-person").animateDockEntry();
  1139. adjustDockSize(1);
  1140. this.detailElement = templating.sub("participant-window", {
  1141. peer: this.peer
  1142. });
  1143. var followId = this.peer.className("togetherjs-person-status-follow-");
  1144. this.detailElement.find('[for="togetherjs-person-status-follow"]').attr("for", followId);
  1145. this.detailElement.find('#togetherjs-person-status-follow').attr("id", followId);
  1146. this.detailElement.find(".togetherjs-follow").click(function () {
  1147. location.href = $(this).attr("href");
  1148. });
  1149. this.detailElement.find(".togetherjs-nudge").click((function () {
  1150. this.peer.nudge();
  1151. }).bind(this));
  1152. this.followCheckbox = this.detailElement.find("#" + followId);
  1153. this.followCheckbox.change(function () {
  1154. if (! this.checked) {
  1155. this.peer.unfollow();
  1156. }
  1157. // Following doesn't happen until the window is closed
  1158. // FIXME: should we tell the user this?
  1159. });
  1160. this.maybeHideDetailWindow = this.maybeHideDetailWindow.bind(this);
  1161. session.on("hide-window", this.maybeHideDetailWindow);
  1162. ui.container.append(this.detailElement);
  1163. this.dockElement.click((function () {
  1164. if (this.detailElement.is(":visible")) {
  1165. windowing.hide(this.detailElement);
  1166. } else {
  1167. windowing.show(this.detailElement, {bind: this.dockElement});
  1168. this.scrollTo();
  1169. this.cursor().element.animate({
  1170. opacity:0.3
  1171. }).animate({
  1172. opacity:1
  1173. }).animate({
  1174. opacity:0.3
  1175. }).animate({
  1176. opacity:1
  1177. });
  1178. }
  1179. }).bind(this));
  1180. this.updateFollow();
  1181. }),
  1182. undock: function () {
  1183. if (! this.dockElement) {
  1184. return;
  1185. }
  1186. this.dockElement.animateDockExit().promise().then((function () {
  1187. this.dockElement.remove();
  1188. this.dockElement = null;
  1189. this.detailElement.remove();
  1190. this.detailElement = null;
  1191. adjustDockSize(-1);
  1192. }).bind(this));
  1193. },
  1194. scrollTo: function () {
  1195. if (this.peer.url != session.currentUrl()) {
  1196. return;
  1197. }
  1198. var pos = this.peer.scrollPosition;
  1199. if (! pos) {
  1200. console.warn("Peer has no scroll position:", this.peer);
  1201. return;
  1202. }
  1203. pos = elementFinder.pixelForPosition(pos);
  1204. $("html, body").easeTo(pos);
  1205. },
  1206. updateFollow: function () {
  1207. if (! this.peer.url) {
  1208. return;
  1209. }
  1210. if (! this.detailElement) {
  1211. return;
  1212. }
  1213. var same = this.detailElement.find(".togetherjs-same-url");
  1214. var different = this.detailElement.find(".togetherjs-different-url");
  1215. if (this.peer.url == session.currentUrl()) {
  1216. same.show();
  1217. different.hide();
  1218. } else {
  1219. same.hide();
  1220. different.show();
  1221. }
  1222. },
  1223. maybeHideDetailWindow: function (windows) {
  1224. if (this.detailElement && windows[0] && windows[0][0] === this.detailElement[0]) {
  1225. if (this.followCheckbox[0].checked) {
  1226. this.peer.follow();
  1227. } else {
  1228. this.peer.unfollow();
  1229. }
  1230. }
  1231. },
  1232. dockClick: function () {
  1233. // FIXME: scroll to person
  1234. },
  1235. cursor: function () {
  1236. return require("cursor").getClient(this.peer.id);
  1237. },
  1238. destroy: function () {
  1239. // FIXME: should I get rid of the dockElement?
  1240. session.off("hide-window", this.maybeHideDetailWindow);
  1241. }
  1242. });
  1243. function updateChatParticipantList() {
  1244. var live = peers.getAllPeers(true);
  1245. if (live.length) {
  1246. ui.displayToggle("#togetherjs-chat-participants");
  1247. $("#togetherjs-chat-participant-list").text(
  1248. live.map(function (p) {return p.name;}).join(", "));
  1249. } else {
  1250. ui.displayToggle("#togetherjs-chat-no-participants");
  1251. }
  1252. }
  1253. function inviteHubUrl() {
  1254. var base = TogetherJS.config.get("inviteFromRoom");
  1255. assert(base);
  1256. return util.makeUrlAbsolute(base, session.hubUrl());
  1257. }
  1258. var inRefresh = false;
  1259. function refreshInvite() {
  1260. if (inRefresh) {
  1261. return;
  1262. }
  1263. inRefresh = true;
  1264. require(["who"], function (who) {
  1265. var def = who.getList(inviteHubUrl());
  1266. function addUser(user, before) {
  1267. var item = templating.sub("invite-user-item", {peer: user});
  1268. item.attr("data-clientid", user.id);
  1269. if (before) {
  1270. item.insertBefore(before);
  1271. } else {
  1272. $("#togetherjs-invite-users").append(item);
  1273. }
  1274. item.click(function() {
  1275. invite(user.clientId);
  1276. });
  1277. }
  1278. function refresh(users, finished) {
  1279. var sorted = [];
  1280. for (var id in users) {
  1281. if (users.hasOwnProperty(id)) {
  1282. sorted.push(users[id]);
  1283. }
  1284. }
  1285. sorted.sort(function (a, b) {
  1286. return a.name < b.name ? -1 : 1;
  1287. });
  1288. var pos = 0;
  1289. ui.container.find("#togetherjs-invite-users .togetherjs-menu-item").each(function () {
  1290. var $this = $(this);
  1291. if (finished && ! users[$this.attr("data-clientid")]) {
  1292. $this.remove();
  1293. return;
  1294. }
  1295. if (pos >= sorted.length) {
  1296. return;
  1297. }
  1298. while (pos < sorted.length && $this.attr("data-clientid") !== sorted[pos].id) {
  1299. addUser(sorted[pos], $this);
  1300. pos++;
  1301. }
  1302. while (pos < sorted.length && $this.attr("data-clientid") == sorted[pos].id) {
  1303. pos++;
  1304. }
  1305. });
  1306. for (var i=pos; i<sorted.length; i++) {
  1307. addUser(sorted[pos]);
  1308. }
  1309. }
  1310. def.then(function (users) {
  1311. refresh(users, true);
  1312. inRefresh = false;
  1313. });
  1314. def.progress(refresh);
  1315. });
  1316. }
  1317. session.hub.on("invite", function (msg) {
  1318. if (msg.forClientId && msg.clientId != peers.Self.id) {
  1319. return;
  1320. }
  1321. require(["who"], function (who) {
  1322. var peer = who.ExternalPeer(msg.userInfo.clientId, msg.userInfo);
  1323. ui.chat.invite({peer: peer, url: msg.url, forEveryone: ! msg.forClientId});
  1324. });
  1325. });
  1326. function invite(clientId) {
  1327. require(["who"], function (who) {
  1328. // FIXME: use the return value of this to give a signal that
  1329. // the invite has been successfully sent:
  1330. who.invite(inviteHubUrl(), clientId).then(function () {
  1331. hideMenu();
  1332. });
  1333. });
  1334. }
  1335. ui.showUrlChangeMessage = deferForContainer(function (peer, url) {
  1336. var window = templating.sub("url-change", {peer: peer});
  1337. ui.container.append(window);
  1338. windowing.show(window);
  1339. });
  1340. session.hub.on("url-change-nudge", function (msg) {
  1341. if (msg.to && msg.to != session.clientId) {
  1342. // Not directed to us
  1343. return;
  1344. }
  1345. msg.peer.urlNudge();
  1346. });
  1347. session.on("new-element", function (el) {
  1348. if (TogetherJS.config.get("toolName")) {
  1349. ui.updateToolName(el);
  1350. }
  1351. });
  1352. var setToolName = false;
  1353. ui.updateToolName = function (container) {
  1354. container = container || $(document.body);
  1355. var name = TogetherJS.config.get("toolName");
  1356. if (setToolName && ! name) {
  1357. name = "TogetherJS";
  1358. }
  1359. if (name) {
  1360. container.find(".togetherjs-tool-name").text(name);
  1361. setToolName = true;
  1362. }
  1363. };
  1364. TogetherJS.config.track("toolName", function (name) {
  1365. ui.updateToolName(ui.container);
  1366. });
  1367. return ui;
  1368. });