You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

750 lines
28 KiB

9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
  1. define([
  2. '/common/common-util.js',
  3. '/common/sframe-common-codemirror.js',
  4. '/customize/messages.js',
  5. '/bower_components/chainpad/chainpad.dist.js',
  6. ], function (Util, SFCodeMirror, Messages, ChainPad) {
  7. var Markers = {};
  8. /* TODO Known Issues
  9. * 1. ChainPad diff is not completely accurate: we're not aware of the other user's cursor
  10. position so if they insert an "a" in the middle of "aaaaa", the diff will think that
  11. the "a" was inserted at the end of this sequence. This is not an issue for the content
  12. but it will cause issues for the colors
  13. 2. ChainPad doesn't always provide the good result in case of conflict (?)
  14. e.g. Alice is inserting "pew" at offset 10, Bob is removing 1 character at offset 10
  15. The expected result is to have "pew" and the following character deleted
  16. In some cases, the result is "ew" inserted and the following character not deleted
  17. */
  18. var debug = function () {};
  19. var MARK_OPACITY = 0.5;
  20. var DEFAULT = {
  21. authors: {},
  22. marks: [[-1, 0, 0, Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER]]
  23. };
  24. var addMark = function (Env, from, to, uid) {
  25. if (!Env.enabled) { return; }
  26. var author = Env.authormarks.authors[uid] || {};
  27. if (uid === -1) {
  28. return void Env.editor.markText(from, to, {
  29. css: "background-color: transparent",
  30. attributes: {
  31. 'data-type': 'authormark',
  32. 'data-uid': uid
  33. }
  34. });
  35. }
  36. uid = Number(uid);
  37. var name = Util.fixHTML(author.name || Messages.anonymous);
  38. var col = Util.hexToRGB(author.color);
  39. var rgba = 'rgba('+col[0]+','+col[1]+','+col[2]+','+Env.opacity+');';
  40. return Env.editor.markText(from, to, {
  41. inclusiveLeft: uid === Env.myAuthorId,
  42. inclusiveRight: uid === Env.myAuthorId,
  43. css: "background-color: " + rgba,
  44. attributes: {
  45. title: Env.opacity ? Messages._getKey('cba_writtenBy', [name]) : '',
  46. 'data-type': 'authormark',
  47. 'data-uid': uid
  48. }
  49. });
  50. };
  51. var sortMarks = function (a, b) {
  52. if (!Array.isArray(b)) { return -1; }
  53. if (!Array.isArray(a)) { return 1; }
  54. // Check line
  55. if (a[1] < b[1]) { return -1; }
  56. if (a[1] > b[1]) { return 1; }
  57. // Same line: check start offset
  58. if (a[2] < b[2]) { return -1; }
  59. if (a[2] > b[2]) { return 1; }
  60. return 0;
  61. };
  62. /* Formats:
  63. [uid, startLine, startCh, endLine, endCh] (multi line)
  64. [uid, startLine, startCh, endCh] (single line)
  65. [uid, startLine, startCh] (single character)
  66. */
  67. var parseMark = Markers.parseMark = function (array) {
  68. if (!Array.isArray(array)) { return {}; }
  69. var multiline = typeof(array[4]) !== "undefined";
  70. var singleChar = typeof(array[3]) === "undefined";
  71. return {
  72. uid: array[0],
  73. startLine: array[1],
  74. startCh: array[2],
  75. endLine: multiline ? array[3] : array[1],
  76. endCh: singleChar ? (array[2]+1) : (multiline ? array[4] : array[3])
  77. };
  78. };
  79. var setAuthorMarks = function (Env, authormarks) {
  80. if (!Env.enabled) {
  81. Env.authormarks = {};
  82. return;
  83. }
  84. authormarks = authormarks || {};
  85. if (!authormarks.marks) { authormarks.marks = Util.clone(DEFAULT.marks); }
  86. if (!authormarks.authors) { authormarks.authors = Util.clone(DEFAULT.authors); }
  87. Env.oldMarks = Env.authormarks;
  88. Env.authormarks = authormarks;
  89. };
  90. var getAuthorMarks = function (Env) {
  91. return Env.authormarks;
  92. };
  93. var updateAuthorMarks = function (Env) {
  94. if (!Env.enabled) { return; }
  95. // get author marks
  96. var _marks = [];
  97. var all = [];
  98. var i = 0;
  99. Env.editor.getAllMarks().forEach(function (mark) {
  100. var pos = mark.find();
  101. var attributes = mark.attributes || {};
  102. if (!pos || attributes['data-type'] !== 'authormark') { return; }
  103. var uid = Number(attributes['data-uid']) || 0;
  104. all.forEach(function (obj) {
  105. if (obj.uid !== uid) { return; }
  106. if (obj.removed) { return; }
  107. // Merge left
  108. if (obj.pos.to.line === pos.from.line && obj.pos.to.ch === pos.from.ch) {
  109. obj.removed = true;
  110. _marks[obj.index] = undefined;
  111. obj.mark.clear();
  112. mark.clear();
  113. mark = addMark(Env, obj.pos.from, pos.to, uid);
  114. pos.from = obj.pos.from;
  115. return;
  116. }
  117. // Merge right
  118. if (obj.pos.from.line === pos.to.line && obj.pos.from.ch === pos.to.ch) {
  119. obj.removed = true;
  120. _marks[obj.index] = undefined;
  121. obj.mark.clear();
  122. mark.clear();
  123. mark = addMark(Env, pos.from, obj.pos.to, uid);
  124. pos.to = obj.pos.to;
  125. }
  126. });
  127. var array = [uid, pos.from.line, pos.from.ch];
  128. if (pos.from.line === pos.to.line && pos.to.ch > (pos.from.ch+1)) {
  129. // If there is more than 1 character, add the "to" character
  130. array.push(pos.to.ch);
  131. } else if (pos.from.line !== pos.to.line) {
  132. // If the mark is on more than one line, add the "to" line data
  133. Array.prototype.push.apply(array, [pos.to.line, pos.to.ch]);
  134. }
  135. _marks.push(array);
  136. all.push({
  137. uid: uid,
  138. pos: pos,
  139. mark: mark,
  140. index: i
  141. });
  142. i++;
  143. });
  144. _marks.sort(sortMarks);
  145. debug('warn', _marks);
  146. Env.authormarks.marks = _marks.filter(Boolean);
  147. };
  148. // Fix all marks located after the given operation in the provided document
  149. var fixMarksFromOp = function (Env, op, marks, doc) {
  150. var pos = SFCodeMirror.posToCursor(op.offset, doc); // pos of start offset
  151. var rPos = SFCodeMirror.posToCursor(op.offset + op.toRemove, doc); // end of removed content
  152. var removed = doc.slice(op.offset, op.offset + op.toRemove).split('\n'); // removed content
  153. var added = op.toInsert.split('\n'); // added content
  154. var posEndLine = pos.line + added.length - 1; // end line after op
  155. var posEndCh = added[added.length - 1].length; // end ch after op
  156. var addLine = added.length - removed.length;
  157. var addCh = added[added.length - 1].length - removed[removed.length - 1].length;
  158. if (addLine > 0) { addCh -= pos.ch; }
  159. else if (addLine < 0) { addCh += pos.ch; }
  160. else { posEndCh += pos.ch; }
  161. var splitted;
  162. marks.forEach(function (mark, i) {
  163. if (!mark) { return; }
  164. var p = parseMark(mark);
  165. // Don't update marks located before the operation
  166. if (p.endLine < pos.line || (p.endLine === pos.line && p.endCh < pos.ch)) { return; }
  167. // Remove markers that have been deleted by my changes
  168. if ((p.startLine > pos.line || (p.startLine === pos.line && p.startCh >= pos.ch)) &&
  169. (p.endLine < rPos.line || (p.endLine === rPos.line && p.endCh <= rPos.ch))) {
  170. marks[i] = undefined;
  171. return;
  172. }
  173. // Update markers that have been cropped right
  174. if (p.endLine < rPos.line || (p.endLine === rPos.line && p.endCh <= rPos.ch)) {
  175. mark[3] = pos.line;
  176. mark[4] = pos.ch;
  177. return;
  178. }
  179. // Update markers that have been cropped left. This markers will be affected by
  180. // my toInsert so don't abort
  181. if (p.startLine < rPos.line || (p.startLine === rPos.line && p.startCh < rPos.ch)) {
  182. // If our change will split an existing mark, put the existing mark after the change
  183. // and create a new mark before
  184. if (p.startLine < pos.line || (p.startLine === pos.line && p.startCh < pos.ch)) {
  185. splitted = [mark[0], mark[1], mark[2], pos.line, pos.ch];
  186. }
  187. mark[1] = rPos.line;
  188. mark[2] = rPos.ch;
  189. }
  190. // Apply my toInsert the to remaining marks
  191. mark[1] += addLine;
  192. if (typeof(mark[4]) !== "undefined") { mark[3] += addLine; }
  193. if (mark[1] === posEndLine) {
  194. mark[2] += addCh;
  195. if (typeof(mark[4]) === "undefined" && typeof(mark[3]) !== "undefined") {
  196. mark[3] += addCh;
  197. } else if (typeof(mark[4]) !== "undefined" && mark[3] === posEndLine) {
  198. mark[4] += addCh;
  199. }
  200. }
  201. });
  202. if (op.toInsert.length) {
  203. marks.push([Env.myAuthorId, pos.line, pos.ch, posEndLine, posEndCh]);
  204. }
  205. if (splitted) {
  206. marks.push(splitted);
  207. }
  208. marks.sort(sortMarks);
  209. };
  210. // Remove marks added by OT and fix the incorrect ones
  211. // first: data about the change with the lowest offset
  212. // last: data about the change with the latest offset
  213. // in the comments, "I" am "first"
  214. var fixMarks = function (Env, first, last, content, toKeepEnd) {
  215. var toKeep = [];
  216. var toJoin = {};
  217. debug('error', "Fix marks");
  218. debug('warn', first);
  219. debug('warn', last);
  220. if (first.me !== last.me) {
  221. // Get their start position compared to the authDoc
  222. var lastAuthOffset = last.offset + last.total;
  223. var lastAuthPos = SFCodeMirror.posToCursor(lastAuthOffset, last.doc);
  224. // Get their start position compared to the localDoc
  225. var lastLocalOffset = last.offset + first.total;
  226. var lastLocalPos = SFCodeMirror.posToCursor(lastLocalOffset, first.doc);
  227. // Keep their changes in the marks (after their offset)
  228. last.marks.some(function (array, i) {
  229. var p = parseMark(array);
  230. // End of the mark before offset? ignore
  231. if (p.endLine < lastAuthPos.line) { return; }
  232. // Take everything from the first mark ending after the pos
  233. if (p.endLine > lastAuthPos.line || p.endCh >= lastAuthPos.ch) {
  234. toKeep = last.marks.slice(i);
  235. last.marks.splice(i);
  236. return true;
  237. }
  238. });
  239. // Keep my marks (based on currentDoc) before their changes
  240. first.marks.some(function (array, i) {
  241. var p = parseMark(array);
  242. // End of the mark before offset? ignore
  243. if (p.endLine < lastLocalPos.line) { return; }
  244. // Take everything from the first mark ending after the pos
  245. if (p.endLine > lastLocalPos.line || p.endCh >= lastLocalPos.ch) {
  246. first.marks.splice(i);
  247. return true;
  248. }
  249. });
  250. }
  251. // If we still have markers in "first", store the last one so that we can "join"
  252. // everything at the end
  253. if (first.marks.length) {
  254. var toJoinMark = first.marks[first.marks.length - 1].slice();
  255. toJoin = parseMark(toJoinMark);
  256. }
  257. // Add the new markers to the result
  258. Array.prototype.unshift.apply(toKeepEnd, toKeep);
  259. debug('warn', toJoin);
  260. debug('warn', toKeep);
  261. debug('warn', toKeepEnd);
  262. // Fix their offset: compute added lines and added characters on the last line
  263. // using the chainpad operation data (toInsert and toRemove)
  264. var pos = SFCodeMirror.posToCursor(first.offset, content);
  265. var removed = content.slice(first.offset, first.offset + first.toRemove).split('\n');
  266. var added = first.toInsert.split('\n');
  267. var posEndLine = pos.line + added.length - 1; // end line after op
  268. var addLine = added.length - removed.length;
  269. var addCh = added[added.length - 1].length - removed[removed.length - 1].length;
  270. if (addLine > 0) { addCh -= pos.ch; }
  271. if (addLine < 0) { addCh += pos.ch; }
  272. toKeepEnd.forEach(function (array) {
  273. // Push to correct lines
  274. array[1] += addLine;
  275. if (typeof(array[4]) !== "undefined") { array[3] += addLine; }
  276. // If they have markers on my end line, push their "ch"
  277. if (array[1] === posEndLine) {
  278. array[2] += addCh;
  279. // If they have no end line, it means end line === start line,
  280. // so we also push their end offset
  281. if (typeof(array[4]) === "undefined" && typeof(array[3]) !== "undefined") {
  282. array[3] += addCh;
  283. } else if (typeof(array[4]) !== "undefined" && array[3] === posEndLine) {
  284. array[4] += addCh;
  285. }
  286. }
  287. });
  288. if (toKeep.length && toJoin && typeof(toJoin.endLine) !== "undefined"
  289. && typeof(toJoin.endCh) !== "undefined") {
  290. // Make sure the marks are joined correctly:
  291. // fix the start position of the marks to keep
  292. // Note: we must preserve the same end for this mark if it was single line!
  293. if (typeof(toKeepEnd[0][4]) === "undefined") { // Single line
  294. toKeepEnd[0][4] = toKeepEnd[0][3] || (toKeepEnd[0][2]+1); // preserve end ch
  295. toKeepEnd[0][3] = toKeepEnd[0][1]; // preserve end line
  296. }
  297. toKeepEnd[0][1] = toJoin.endLine;
  298. toKeepEnd[0][2] = toJoin.endCh;
  299. }
  300. debug('log', 'Fixed');
  301. debug('warn', toKeepEnd);
  302. };
  303. var checkMarks = function (Env, userDoc) {
  304. var chainpad = Env.framework._.cpNfInner.chainpad;
  305. var editor = Env.editor;
  306. var CodeMirror = Env.CodeMirror;
  307. Env.enabled = Boolean(userDoc.authormarks && userDoc.authormarks.marks);
  308. setAuthorMarks(Env, userDoc.authormarks);
  309. if (!Env.enabled) { return; }
  310. debug('error', 'Check marks');
  311. var authDoc = JSON.parse(chainpad.getAuthDoc() || '{}');
  312. if (!authDoc.content || !userDoc.content) { return; }
  313. var authPatch = chainpad.getAuthBlock();
  314. if (authPatch.isFromMe) {
  315. debug('log', 'Switch branch, from me');
  316. debug('log', authDoc.content);
  317. debug('log', authDoc.authormarks.marks);
  318. debug('log', userDoc.content);
  319. // We're switching to a different branch that was created by us.
  320. // We can't trust localDoc anymore because it contains data from the other branch
  321. // It means the only changes that we need to consider are ours.
  322. // Diff between userDoc and authDoc to see what we changed
  323. var _myOps = ChainPad.Diff.diff(authDoc.content, userDoc.content).reverse();
  324. var authormarks = Util.clone(authDoc.authormarks);
  325. _myOps.forEach(function (op) {
  326. fixMarksFromOp(Env, op, authormarks.marks, authDoc.content);
  327. });
  328. authormarks.marks = authormarks.marks.filter(Boolean);
  329. debug('log', 'Fixed marks');
  330. debug('warn', authormarks.marks);
  331. setAuthorMarks(Env, authormarks);
  332. return;
  333. }
  334. var oldMarks = Env.oldMarks;
  335. if (authDoc.content === userDoc.content) { return; } // No uncommitted work
  336. if (!userDoc.authormarks || !Array.isArray(userDoc.authormarks.marks)) { return; }
  337. debug('warn', 'Begin...');
  338. var localDoc = CodeMirror.canonicalize(editor.getValue());
  339. var commonParent = chainpad.getAuthBlock().getParent().getContent().doc;
  340. var content = JSON.parse(commonParent || '{}').content || '';
  341. var theirOps = ChainPad.Diff.diff(content, authDoc.content);
  342. var myOps = ChainPad.Diff.diff(content, localDoc);
  343. debug('log', theirOps);
  344. debug('log', myOps);
  345. if (!myOps.length || !theirOps.length) { return; }
  346. // If I have uncommited content when receiving a remote patch, all the operations
  347. // placed after someone else's changes will create marker issues. We have to fix it
  348. var sorted = [];
  349. var myTotal = 0;
  350. var theirTotal = 0;
  351. var parseOp = function (me) {
  352. return function (op) {
  353. var size = (op.toInsert.length - op.toRemove);
  354. sorted.push({
  355. me: me,
  356. offset: op.offset,
  357. toInsert: op.toInsert,
  358. toRemove: op.toRemove,
  359. size: size,
  360. marks: (me ? (oldMarks && oldMarks.marks)
  361. : (authDoc.authormarks && authDoc.authormarks.marks)) || [],
  362. doc: me ? localDoc : authDoc.content
  363. });
  364. if (me) { myTotal += size; }
  365. else { theirTotal += size; }
  366. };
  367. };
  368. myOps.forEach(parseOp(true));
  369. theirOps.forEach(parseOp(false));
  370. // Sort the operation in reverse order of offset
  371. // If an operation from them has the same offset than an operation from me, put mine first
  372. sorted.sort(function (a, b) {
  373. if (a.offset === b.offset) {
  374. return a.me ? -1 : 1;
  375. }
  376. return b.offset - a.offset;
  377. });
  378. debug('log', sorted);
  379. // We start from the end so that we don't have to fix the offsets everytime
  380. var prev;
  381. var toKeepEnd = [];
  382. sorted.forEach(function (op) {
  383. // Not the same author? fix!
  384. if (prev) {
  385. // Provide the new "totals"
  386. prev.total = prev.me ? myTotal : theirTotal;
  387. op.total = op.me ? myTotal : theirTotal;
  388. // Fix the markers
  389. fixMarks(Env, op, prev, content, toKeepEnd);
  390. }
  391. if (op.me) { myTotal -= op.size; }
  392. else { theirTotal -= op.size; }
  393. prev = op;
  394. });
  395. debug('log', toKeepEnd);
  396. // We now have all the markers located after the first operation (ordered by offset).
  397. // Prepend the markers placed before this operation
  398. var first = sorted[sorted.length - 1];
  399. if (first) { Array.prototype.unshift.apply(toKeepEnd, first.marks); }
  400. // Commit our new markers
  401. Env.authormarks.marks = toKeepEnd;
  402. debug('warn', toKeepEnd);
  403. debug('warn', '...End');
  404. };
  405. // Reset marks displayed in CodeMirror to the marks stored in Env
  406. var setMarks = function (Env) {
  407. // on remote update: remove all marks, add new marks if colors are enabled
  408. Env.editor.getAllMarks().forEach(function (marker) {
  409. if (marker.attributes && marker.attributes['data-type'] === 'authormark') {
  410. marker.clear();
  411. }
  412. });
  413. if (!Env.enabled) { return; }
  414. debug('error', 'setMarks');
  415. debug('log', Env.authormarks.marks);
  416. var authormarks = Env.authormarks;
  417. authormarks.marks.forEach(function (mark) {
  418. var uid = mark[0];
  419. if (uid !== -1 && (!authormarks.authors || !authormarks.authors[uid])) { return; }
  420. var from = {};
  421. var to = {};
  422. from.line = mark[1];
  423. from.ch = mark[2];
  424. if (mark.length === 3) {
  425. to.line = mark[1];
  426. to.ch = mark[2]+1;
  427. } else if (mark.length === 4) {
  428. to.line = mark[1];
  429. to.ch = mark[3];
  430. } else if (mark.length === 5) {
  431. to.line = mark[3];
  432. to.ch = mark[4];
  433. }
  434. // Remove marks that are placed under this one
  435. try {
  436. Env.editor.findMarks(from, to).forEach(function (mark) {
  437. if (!mark || !mark.attributes || mark.attributes['data-type'] !== 'authormark') { return; }
  438. mark.clear();
  439. });
  440. } catch (e) {
  441. console.warn(mark, JSON.stringify(authormarks.marks));
  442. console.error(from, to);
  443. console.error(e);
  444. }
  445. addMark(Env, from, to, uid);
  446. });
  447. };
  448. var setMyData = function (Env) {
  449. if (!Env.enabled) { return; }
  450. var userData = Env.common.getMetadataMgr().getUserData();
  451. var old = Env.authormarks.authors[Env.myAuthorId];
  452. Env.authormarks.authors[Env.myAuthorId] = {
  453. name: userData.name,
  454. curvePublic: userData.curvePublic,
  455. color: userData.color
  456. };
  457. if (!old || (old.name === userData.name && old.color === userData.color)) { return; }
  458. return true;
  459. };
  460. var localChange = function (Env, change, cb) {
  461. cb = cb || function () {};
  462. if (!Env.enabled) { return void cb(); }
  463. debug('error', 'Local change');
  464. debug('log', change, true);
  465. if (change.origin === "setValue") {
  466. // If the content is changed from a remote patch, we call localChange
  467. // in "onContentUpdate" directly
  468. return;
  469. }
  470. if (change.text === undefined || ['+input', 'paste'].indexOf(change.origin) === -1) {
  471. return void cb();
  472. }
  473. // add new author mark if text is added. marks from removed text are removed automatically
  474. // change.to is not always correct, fix it!
  475. var to_add = {
  476. line: change.from.line + change.text.length-1,
  477. };
  478. if (change.text.length > 1) {
  479. // Multiple lines => take the length of the text added to the last line
  480. to_add.ch = change.text[change.text.length-1].length;
  481. } else {
  482. // Single line => use the "from" position and add the length of the text
  483. to_add.ch = change.from.ch + change.text[change.text.length-1].length;
  484. }
  485. // If my text is inside an existing mark:
  486. // * if it's my mark, do nothing
  487. // * if it's someone else's mark, break it
  488. // We can only have one author mark at a given position, but there may be
  489. // another mark (cursor selection...) at this position so we use ".some"
  490. var toSplit, abort;
  491. Env.editor.findMarks(change.from, to_add).some(function (mark) {
  492. if (!mark.attributes) { return; }
  493. if (mark.attributes['data-type'] !== 'authormark') { return; }
  494. if (mark.attributes['data-uid'] !== Env.myAuthorId) {
  495. toSplit = {
  496. mark: mark,
  497. uid: mark.attributes['data-uid']
  498. };
  499. } else {
  500. // This is our mark: abort to avoid making a new one
  501. abort = true;
  502. }
  503. return true;
  504. });
  505. if (abort) { return void cb(); }
  506. // Add my data to the doc if it's missing
  507. if (!Env.authormarks.authors[Env.myAuthorId]) {
  508. setMyData(Env);
  509. }
  510. if (toSplit && toSplit.mark && typeof(toSplit.uid) !== "undefined") {
  511. // Break the other user's mark if needed
  512. var _pos = toSplit.mark.find();
  513. toSplit.mark.clear();
  514. addMark(Env, _pos.from, change.from, toSplit.uid); // their mark, 1st part
  515. addMark(Env, change.from, to_add, Env.myAuthorId); // my mark
  516. addMark(Env, to_add, _pos.to, toSplit.uid); // their mark, 2nd part
  517. } else {
  518. // Add my mark
  519. addMark(Env, change.from, to_add, Env.myAuthorId);
  520. }
  521. cb();
  522. };
  523. var setButton = function (Env, $button) {
  524. var toggle = function () {
  525. var tippy = $button[0] && $button[0]._tippy;
  526. if (Env.opacity) {
  527. Env.opacity = 0;
  528. if (tippy) { tippy.title = Messages.cba_show; }
  529. else { $button.attr('title', Messages.cba_show); }
  530. $button.removeClass("cp-toolbar-button-active");
  531. } else {
  532. Env.opacity = MARK_OPACITY;
  533. if (tippy) { tippy.title = Messages.cba_hide; }
  534. else { $button.attr('title', Messages.cba_hide); }
  535. $button.addClass("cp-toolbar-button-active");
  536. }
  537. };
  538. toggle();
  539. Env.$button = $button;
  540. $button.click(function() {
  541. toggle();
  542. setMarks(Env);
  543. });
  544. };
  545. var authorUid = function (existing) {
  546. if (!Array.isArray(existing)) { existing = []; }
  547. var n;
  548. var i = 0;
  549. while (!n || existing.indexOf(n) !== -1 && i++ < 1000) {
  550. n = Math.floor(Math.random() * 1000000);
  551. }
  552. // If we can't find a valid number in 1000 iterations, use 0...
  553. if (existing.indexOf(n) !== -1) { n = 0; }
  554. return n;
  555. };
  556. var getAuthorId = function (Env) {
  557. var existing = Object.keys(Env.authormarks.authors || {}).map(Number);
  558. if (!Env.common.isLoggedIn()) { return authorUid(existing); }
  559. var userData = Env.common.getMetadataMgr().getUserData();
  560. var uid;
  561. existing.some(function (id) {
  562. var author = Env.authormarks.authors[id] || {};
  563. if (author.curvePublic !== userData.curvePublic) { return; }
  564. uid = Number(id);
  565. return true;
  566. });
  567. return uid || authorUid(existing);
  568. };
  569. var ready = function (Env) {
  570. Env.ready = true;
  571. Env.myAuthorId = getAuthorId(Env);
  572. if (!Env.enabled) { return; }
  573. if (Env.$button) { Env.$button.show(); }
  574. if (!Env.authormarks.marks || !Env.authormarks.marks.length) {
  575. Env.authormarks = Util.clone(DEFAULT);
  576. }
  577. setMarks(Env);
  578. };
  579. var getState = function (Env) {
  580. return Boolean(Env.authormarks && Env.authormarks.marks);
  581. };
  582. var setState = function (Env, enabled) {
  583. // If the state has changed in the pad, change the Env too
  584. if (!Env.ready) { return; }
  585. if (Env.enabled === enabled) { return; }
  586. Env.enabled = enabled;
  587. if (!Env.enabled) {
  588. // Reset marks
  589. Env.authormarks = {};
  590. setMarks(Env);
  591. if (Env.$button) { Env.$button.hide(); }
  592. } else {
  593. Env.myAuthorId = getAuthorId(Env);
  594. // If it's a reset, add initial marker
  595. if (!Env.authormarks.marks || !Env.authormarks.marks.length) {
  596. Env.authormarks = Util.clone(DEFAULT);
  597. setMarks(Env);
  598. }
  599. if (Env.$button) { Env.$button.show(); }
  600. }
  601. if (Env.ready) { Env.framework.localChange(); }
  602. };
  603. Markers.create = function (config) {
  604. var Env = config;
  605. Env.authormarks = {};
  606. Env.enabled = false;
  607. Env.myAuthorId = 0;
  608. if (Env.devMode) {
  609. debug = function (level, obj, logObject) {
  610. var f = console.log;
  611. if (typeof(console[level]) === "function") {
  612. f = console[level];
  613. }
  614. if (logObject) { return void f(obj); }
  615. };
  616. }
  617. var metadataMgr = Env.common.getMetadataMgr();
  618. metadataMgr.onChange(function () {
  619. // If the markers are disabled or if I haven't pushed content since the last reset,
  620. // don't update my data
  621. if (!Env.enabled || !Env.myAuthorId || !Env.authormarks.authors ||
  622. !Env.authormarks.authors[Env.myAuthorId]) {
  623. return;
  624. }
  625. // Update my data
  626. var changed = setMyData(Env);
  627. if (changed) {
  628. setMarks(Env);
  629. Env.framework.localChange();
  630. }
  631. });
  632. var call = function (f) {
  633. return function () {
  634. try {
  635. [].unshift.call(arguments, Env);
  636. return f.apply(null, arguments);
  637. } catch (e) {
  638. console.error(e);
  639. }
  640. };
  641. };
  642. return {
  643. addMark: call(addMark),
  644. getAuthorMarks: call(getAuthorMarks),
  645. updateAuthorMarks: call(updateAuthorMarks),
  646. checkMarks: call(checkMarks),
  647. setMarks: call(setMarks),
  648. localChange: call(localChange),
  649. ready: call(ready),
  650. setButton: call(setButton),
  651. getState: call(getState),
  652. setState: call(setState),
  653. };
  654. };
  655. return Markers;
  656. });