PageRenderTime 60ms CodeModel.GetById 24ms RepoModel.GetById 0ms app.codeStats 0ms

/graphingwiki/htdocs/gwikicommon/js/MetaTable.js

https://bitbucket.org/clarifiednetworks/graphingwiki/
JavaScript | 737 lines | 595 code | 125 blank | 17 comment | 80 complexity | 1d72658e52daa04b967ade10b1d0e417 MD5 | raw file
  1. /*
  2. MetaTable.js
  3. - js improvements for MetaTable
  4. License: MIT <http://www.opensource.org/licenses/mit-license.php>
  5. Author: Lauri Pokka
  6. Depends: MooTools HtmlTable.sort InlineEditor Request.SetMetas Events.shiftclick More/Date
  7. provides: [gwiki.MetaTable, gwiki.InterMetaTable]
  8. */
  9. define([
  10. './InlineEditor',
  11. './DynamicTextarea',
  12. './Overlay',
  13. 'mootools-more'
  14. ], function(InlineEditor, dt, Overlay) {
  15. "use strict";
  16. var DynamicTextarea = dt.DynamicTextarea,
  17. $ = document.id;
  18. var preformatTable = function(tab) {
  19. var head = tab.getElement('thead');
  20. if (!head) {
  21. head = new Element('thead').inject(tab, 'top');
  22. if (!head.getElement('tr')) {
  23. head.grab(tab.getElement('tr'));
  24. }
  25. }
  26. head.getElements('td')
  27. .addClass('head_cell');
  28. return tab;
  29. };
  30. var diff = function(o1, o2) {
  31. var changed = [];
  32. Object.each(o2, function(metas, page) {
  33. if (!o1[page]) changed.include(page);
  34. else {
  35. Object.each(metas, function(values, key) {
  36. var values2 = o1[page][key];
  37. if (!values2 || !values.every(values2.contains.bind(values2)) || !values2.every(values.contains.bind(values)))
  38. changed.include(page);
  39. });
  40. }
  41. });
  42. return changed;
  43. };
  44. var retrieveMetas = function(tbody) {
  45. var metas = {};
  46. tbody.getElements('tr').each(function(row) {
  47. var page = row.getElement('.meta_page');
  48. if (page) page = page.get('text');
  49. if (page && !metas[page]) metas[page] = {};
  50. var vals = {};
  51. row.getElements('.meta_cell span[data-key]').each(function(span) {
  52. var val = span.get('data-value') || span.get('text');
  53. var page = span.get('data-page');
  54. var key = span.get('data-key');
  55. var index = span.get('data-index');
  56. if (page && !metas[page]) metas[page] = {};
  57. if (val) {
  58. if (!vals[key]) vals[key] = {};
  59. vals[key][index] = val;
  60. metas[page][key] = Object.values(vals[key]);
  61. }
  62. });
  63. });
  64. return metas;
  65. };
  66. HtmlTable.Parsers['numberSpan'] = {
  67. match: /^<[^>]+>\d+[^\d.,]*<.+/,
  68. convert: function() {
  69. return this.getElement('span').get('text').toInt();
  70. },
  71. number: true
  72. };
  73. if (!HtmlTable.ParserPriority.contains("numberSpan")) {
  74. HtmlTable.ParserPriority.splice(0, 0, "numberSpan")
  75. }
  76. var HideableTable = new Class({
  77. Extends: HtmlTable,
  78. initialize: function() {
  79. this.parent.apply(this, arguments);
  80. this.hiddenCells = [];
  81. },
  82. enableHiding: function() {
  83. this.disableHiding();
  84. this.head.getElements('td, th').each(function(td) {
  85. var style = {
  86. //'float': 'right',
  87. 'color': 'green',
  88. 'cursor': 'pointer',
  89. 'margin-left': '4px',
  90. 'margin-right': 0
  91. };
  92. td.getElement('div').grab(new Element('a', {
  93. 'html': '&#171;',
  94. 'class': 'hidelink',
  95. 'title': 'hide column',
  96. 'styles': style
  97. }));
  98. td.grab(new Element('a', {
  99. 'html': '&#187;',
  100. 'class': 'showlink',
  101. 'title': 'show column',
  102. 'styles': style
  103. }).setStyle('display', 'none'));
  104. return this;
  105. });
  106. this.head.addEvents({
  107. 'click:relay(a.hidelink)': function(event) {
  108. this.hide.apply(this, [this.head.getElements('a.hidelink').indexOf(event.target)]);
  109. }.bind(this),
  110. 'click:relay(a.showlink)': function(event) {
  111. this.show.apply(this, [this.head.getElements('a.showlink').indexOf(event.target)]);
  112. }.bind(this)
  113. });
  114. return this;
  115. },
  116. disableHiding: function() {
  117. this.head.getElements('a.hidelink, a.showlink').destroy();
  118. this.head.removeEvents('click:relay(a.hidelink)');
  119. this.head.removeEvents('click:relay(a.showlink)');
  120. return this;
  121. },
  122. hide: function(i) {
  123. this.hiddenCells.include(i);
  124. i++;
  125. this.head.getElements(this.options.thSelector + ':nth-child(' + i + ') div').setStyle('display', 'none');
  126. this.head.getElements(this.options.thSelector + ':nth-child(' + i + ') .showlink').setStyle('display', '');
  127. this.body.getElements('tr td:nth-child(' + i + ')').each(function(td) {
  128. td.store('hiddenHtml', td.get('html'));
  129. td.set('html', '');
  130. });
  131. return false;
  132. },
  133. show: function(i) {
  134. this.hiddenCells.erase(i);
  135. i++;
  136. this.head.getElements(this.options.thSelector + ':nth-child(' + i + ') div').setStyle('display', '');
  137. this.head.getElements(this.options.thSelector + ':nth-child(' + i + ') .showlink').setStyle('display', 'none');
  138. this.body.getElements('tr td:nth-child(' + i + ')').each(function(td) {
  139. td.set('html', td.retrieve('hiddenHtml'));
  140. });
  141. return false;
  142. },
  143. headClick: function(event) {
  144. //clear unintended selections
  145. if (window.getSelection) {
  146. var s = window.getSelection();
  147. if (s) s.collapse(document.body, 0);
  148. }
  149. if (!$(event.target).hasClass('hidelink') && !$(event.target).hasClass('showlink'))
  150. this.parent.apply(this, arguments);
  151. }
  152. });
  153. var EditableTable = new Class({
  154. enableValueEdit: function() {
  155. var selectors = [".meta_cell span[data-key]:not(.edit)", ".meta_cell:not(.edit)"];
  156. this.body.addEvent('shiftclick:relay(' + selectors.join(", ") + ')', this.valueEdit.bind(this));
  157. },
  158. enableKeyEdit: function() {
  159. this.head.addEvent('shiftclick:relay(span[data-key])', this.keyEdit.bind(this));
  160. },
  161. isUpdating: function() {
  162. return false;
  163. },
  164. valueEdit: function(event) {
  165. event.preventDefault();
  166. if (this.isUpdating()) {
  167. this.valueEdit.delay(100, this, event);
  168. return;
  169. }
  170. var target = $(event.target),
  171. baseUrl = this.options.baseurl,
  172. key, index, page, collab, oldValue = "", metas;
  173. if (target.get('tag') == 'td') {
  174. //add new value, page and key can be retrieved from first span
  175. if (target.children.length > 0) {
  176. var first = target.getElement('span');
  177. page = first.get('data-page');
  178. key = first.get('data-key');
  179. collab = first.get('data-collab');
  180. metas = collab ? this.metas[collab] : this.metas;
  181. //only one span without value => use that span
  182. if (target.children.length == 1 && first.get('text') === "") {
  183. index = 0;
  184. target = first;
  185. } else {
  186. //append new value to existing ones if key had values
  187. index = metas[page][key].length;
  188. target = new Element('span').inject(target);
  189. }
  190. } else {
  191. return;
  192. }
  193. } else {
  194. //edit existing value
  195. if (!target.get('data-key')) target = target.getParent('span[data-key]');
  196. page = target.get('data-page');
  197. key = target.get('data-key');
  198. index = target.get('data-index');
  199. collab = target.get('data-collab');
  200. metas = collab ? this.metas[collab] : this.metas;
  201. oldValue = metas[page][key][index];
  202. }
  203. if (this.inlineEditor) this.inlineEditor.cancel();
  204. var editor = this.inlineEditor = new InlineEditor(target, {
  205. autoFormat: false,
  206. oldValue: oldValue,
  207. key: key,
  208. compact: true,
  209. onSave: function(value) {
  210. new Request.SetMetas2({
  211. url: (collab && baseUrl) ? baseUrl + collab + "/" : "",
  212. onComplete: function() {
  213. this.refresh([collab]);
  214. }.bind(this)
  215. }).send([
  216. {op: 'del', key: key, value: oldValue, page: page},
  217. {op: 'add', key: key, value: value, page: page}
  218. ]);
  219. this.inlineEditor = null;
  220. editor.exit();
  221. }.bind(this)
  222. });
  223. },
  224. keyEdit: function(event) {
  225. event.stopPropagation();
  226. event.preventDefault();
  227. if (this.isUpdating()) {
  228. this.keyEdit.delay(100, this, event);
  229. return;
  230. }
  231. var target = $(event.target);
  232. if (!target.get('data-key')) target = target.getParent('span[data-key]');
  233. var oldKey = target.get('data-key');
  234. //check that the key is really meta-key and not indirection
  235. if (!Object.some(this.metas, function(metas, page) {
  236. return metas[oldKey];
  237. })) return;
  238. if (this.inlineEditor) this.inlineEditor.cancel();
  239. var parent = target.getParent('td').addClass('edit');
  240. var editor = this.inlineEditor = new InlineEditor(target, {
  241. autoFormat: false,
  242. oldValue: oldKey,
  243. compact: true,
  244. onSave: function(newKey) {
  245. var changes = [];
  246. Object.each(this.metas, function(metas, page) {
  247. if (metas[oldKey]) {
  248. changes.push({op: 'set', page: page, key: oldKey});
  249. changes.push({op: 'add', page: page, key: newKey, value: metas[oldKey]});
  250. }
  251. });
  252. new Request.SetMetas2({
  253. onComplete: function() {
  254. this.refresh();
  255. }.bind(this),
  256. onFailure: function() {
  257. alert("Could not edit the key!");
  258. }.bind(this)
  259. }).send(changes);
  260. this.inlineEditor = null;
  261. editor.exit();
  262. }.bind(this),
  263. onExit: function() {
  264. parent.removeClass('edit');
  265. }
  266. });
  267. }
  268. });
  269. var MetaTable = new Class({
  270. Extends: HideableTable,
  271. Implements: [EditableTable],
  272. options: {
  273. thSelector: 'td.head_cell:not(.edit)',
  274. tableArguments: {}
  275. },
  276. initialize: function(container, options) {
  277. container = $(container);
  278. var table = container.getElement('table');
  279. preformatTable(table);
  280. this.parent.call(this, table, options);
  281. this.tableArgs = Object.merge({
  282. 'args': '',
  283. 'template': null,
  284. 'autorefresh': false,
  285. 'nametemplate': ''
  286. }, this.options.tableArguments);
  287. this.metaRequest = new Request.HTML();
  288. this.metas = retrieveMetas(this.body);
  289. this.enableValueEdit();
  290. this.enableKeyEdit();
  291. if (this.tableArgs.autorefresh) {
  292. this.refresh.periodical(this.tableArgs.autorefresh * 1000, this, null);
  293. }
  294. if (container.getElement('.meta_footer_link')) {
  295. new Element('a.jslink[text=[new row]]')
  296. .setStyles({'font-size': 'inherit'})
  297. .addEvent('click', this.newPage.bind(this))
  298. .inject(container.getElement('.meta_footer_link'), 'before');
  299. }
  300. this.enableSort();
  301. this.enableHiding();
  302. },
  303. setTable: function(tab) {
  304. tab = preformatTable(tab);
  305. this.head.getElements(this.options.thSelector).destroy();
  306. this.head.adopt(tab.getElement('thead').getElements(this.options.thSelector));
  307. this.body.getElements('tr').destroy();
  308. this.body.adopt(tab.getElement('tbody').getElements('tr'));
  309. this.setParsers();
  310. this.enableHiding();
  311. return this;
  312. },
  313. newPage: function(event) {
  314. if (event) event.preventDefault();
  315. if (this.tableArgs.template && !this.template) {
  316. if (!this._templateRequest) {
  317. this._templateRequest = new Request({
  318. url: '?action=ajaxUtils&util=getTemplate&name=' + encodeURIComponent(this.tableArgs.template),
  319. onSuccess: function(txt) {
  320. this.template = txt;
  321. delete this['_templateRequest'];
  322. }.bind(this)
  323. }).send();
  324. }
  325. this.newPage.delay(100, this);
  326. return;
  327. }
  328. var editor = new Editor({
  329. content: this.template,
  330. name: this.tableArgs.nametemplate,
  331. template: this.tableArgs.template
  332. });
  333. editor.addEvent('success', function() {
  334. this.refresh();
  335. }.bind(this));
  336. },
  337. isUpdating: function() {
  338. return this.metaRequest.isRunning();
  339. },
  340. refresh: function() {
  341. if (this.inlineEditor) return;
  342. var oldMetas = Object.clone(this.metas);
  343. this.metaRequest = new Request.HTML({
  344. url: '?action=showMetaTable',
  345. data: 'args=' + encodeURIComponent(this.tableArgs.args),
  346. evalScripts: false,
  347. onSuccess: function(nodes) {
  348. var tab = $$(nodes).filter(function(n) {
  349. return typeOf(n.getElement) == "function";
  350. }).getElement('table').clean();
  351. if (tab.length != 1) return; //todo: show error message
  352. this.setTable(tab[0]);
  353. this.reSort();
  354. this.hiddenCells.each(this.hide, this);
  355. this.metas = retrieveMetas(this.body);
  356. var highlightChanges = function() {
  357. diff(oldMetas, this.metas).each(function(page) {
  358. this.body.getElements('tr').each(function(row) {
  359. if (row.cells[0].get('text') == page) {
  360. row.highlight("#bfb");
  361. }
  362. });
  363. }, this);
  364. }.bind(this);
  365. highlightChanges.delay(500);
  366. }.bind(this)
  367. }).get();
  368. }
  369. });
  370. var Editor = new Class({
  371. Extends: Overlay,
  372. options: {
  373. content: "",
  374. name: "",
  375. template: ""
  376. },
  377. build: function() {
  378. this.parent();
  379. var form = new Element('form').inject(this.editor);
  380. var namefield = new Element('input[name=pagename][placeholder=Page Name]')
  381. .set('value', new Date().format(this.options.name))
  382. .setStyles({'width': '200px', 'margin-left': '20px'});
  383. form.adopt(
  384. new Element('span[text=Pagename: ]'),
  385. namefield,
  386. new Element('br'),
  387. new Element('br')
  388. );
  389. if (this.options.template) {
  390. var template = encodeURIComponent(this.options.template);
  391. new Element('a', {
  392. 'text': 'create in metaformedit',
  393. 'target': '_blank',
  394. 'href': namefield.get('value') + '?action=editmetaform&template=' + template,
  395. 'styles': {
  396. 'margin-left': '10px'
  397. },
  398. 'events': {
  399. 'click': function() {
  400. this.cancel();
  401. }.bind(this)
  402. }
  403. }).inject(namefield, 'after');
  404. }
  405. this.textarea = new Element('textarea', {
  406. 'value': this.options.content,
  407. 'styles': {
  408. 'width': '100%',
  409. 'margin': 'auto'
  410. }
  411. }).inject(form);
  412. new DynamicTextarea(this.textarea, {
  413. minRows: 20
  414. });
  415. var controls = new Element('div.clearfix').adopt(
  416. new Element('button.btn.btn-primary', {
  417. text: 'Save',
  418. styles: {
  419. 'float': 'left',
  420. 'margin-top': '10px'
  421. },
  422. events: {
  423. 'click': this.send.bind(this)
  424. }
  425. })
  426. ).inject(this.container);
  427. },
  428. send: function() {
  429. var page = this.editor.getElement('input[name=pagename]').value;
  430. var data = {
  431. action: 'ajaxUtils',
  432. util: 'newPage',
  433. page: page,
  434. content: this.textarea.get('value')
  435. };
  436. new Request.JSON({
  437. url: "",
  438. method: 'post',
  439. data: Object.toQueryString(data),
  440. onSuccess: function(response) {
  441. if (response && response.status == "ok") {
  442. this.fireEvent('success');
  443. this.cancel();
  444. } else {
  445. alert("Failed to create new page!\n" + '"' + response.msg + '"');
  446. }
  447. }.bind(this)
  448. }).send();
  449. }
  450. });
  451. var InterMetaTable = new Class({
  452. Extends: HideableTable,
  453. Implements: [EditableTable],
  454. options: {
  455. selector: "",
  456. baseurl: "",
  457. sortBy: "",
  458. sortDir: 'asc',
  459. collabs: [""],
  460. keys: [],
  461. footer: false,
  462. inaccessibleCollabs: null,
  463. action: "getMetaJSON"
  464. },
  465. initialize: function(el, opts) {
  466. this.container = $(el);
  467. ["_format", "construct"].each(function(f) {
  468. this[f].bind(this);
  469. }, this);
  470. this.parent.apply(this, [null, opts]);
  471. this.enableSort();
  472. this.enableValueEdit();
  473. ["sortBy", "sortDir"].forEach(function(key) {
  474. if (typeOf(this.options[key]) == "array") this.options[key] = this.options[key][0];
  475. }, this);
  476. this.refresh();
  477. },
  478. build: function() {
  479. this.parent();
  480. this.inject(this.container);
  481. this.container.addClass('waiting');
  482. var denied = this.options.inaccessibleCollabs;
  483. if (denied) {
  484. this.container.grab(new Element('em', {
  485. text: 'You do not have premission to access following collab' +
  486. (denied.length > 1 ? "s" : "") + ': ' + denied,
  487. styles: {
  488. color: 'red'
  489. }
  490. }), 'top');
  491. }
  492. //this.container.grab(new Element('a.jslink[text=[settings]]'), 'bottom')
  493. },
  494. _format: function(vals, f) {
  495. if (typeOf(vals) != "array") vals = [vals];
  496. return vals.map(function(value) {
  497. return f[value] || value;
  498. }, this);
  499. },
  500. construct: function() {
  501. this.empty();
  502. var keys = {};
  503. Object.each(this.metas, function(pages, collab) {
  504. Object.each(pages, function(metas, page) {
  505. Object.keys(metas).each(function(key) {
  506. if (!keys[key]) keys[key] = this._format(key, this.formatted[collab]);
  507. }, this);
  508. }, this);
  509. }, this);
  510. var foots = {};
  511. var sortedKeys;
  512. if (this.options.keys.length === 0) {
  513. sortedKeys = Object.sortedKeys(keys);
  514. } else {
  515. sortedKeys = this.options.keys;
  516. sortedKeys.forEach(function(key) {
  517. if (!keys[key]) keys[key] = key
  518. });
  519. }
  520. sortedKeys.each(function(key) {
  521. foots[key] = 0;
  522. });
  523. //this.setHeaders(([new Element('a.jslink[text=edit]')].concat(Object.sortedValues(keys))));
  524. keys = [""].concat(sortedKeys.map(function(key) {
  525. return keys[key];
  526. }).flatten());
  527. this.setHeaders(keys);
  528. this.thead.rows[0].addClass('meta_header');
  529. var genEl = function(collab, page, key, index, html) {
  530. return new Element('span', {
  531. 'html': html,
  532. 'data-collab': collab,
  533. 'data-page': page,
  534. 'data-key': key,
  535. 'data-index': index
  536. });
  537. };
  538. Object.each(this.metas, function(pages, collab) {
  539. var pagePrefix = this.options.collabs.length == 1
  540. && this.options.collabs[0] == this.options.active ?
  541. "": collab + ':';
  542. Object.each(pages, function(metas, page) {
  543. var vals = [new Element('a', {
  544. text: pagePrefix + page,
  545. href: this.options.baseurl + collab + '/' + page
  546. })];
  547. sortedKeys.each(function(key) {
  548. if (!metas[key] || metas[key].length === 0) {
  549. vals.push(genEl(collab, page, key, 0, ""));
  550. } else {
  551. var formatted = this._format(metas[key], this.formatted[collab]);
  552. vals.push(formatted.map(function(html, index) {
  553. return genEl(collab, page, key, index, html).outerHTML;
  554. }).join(", "));
  555. if (Number.from(metas[key])) {
  556. foots[key] += Number.from(metas[key]);
  557. } else {
  558. foots[key] = null;
  559. }
  560. }
  561. }, this);
  562. this.push(new Element('tr').adopt(
  563. vals.map(function(el) {
  564. return new Element('td', {
  565. html: el.outerHTML ? el.outerHTML : el
  566. })
  567. })
  568. ));
  569. }, this);
  570. }, this);
  571. $$(this.body.rows).getFirst('td').addClass('meta_page');
  572. $$($$(this.body.rows).getElements('td:not(.meta_page)')).addClass('meta_cell');
  573. this.sort(keys.indexOf(this.options.sortBy), this.options.sortDir == "desc");
  574. this.enableHiding();
  575. if (!this.isUpdating()) this.container.removeClass('waiting');
  576. },
  577. isUpdating: function() {
  578. if (!this.requests || this.requests.length === 0) return false;
  579. return this.requests.every(function(request) {
  580. return request.isRunning();
  581. });
  582. },
  583. refresh: function(collabs) {
  584. collabs = collabs || this.options.collabs;
  585. this.requests = [];
  586. collabs.each(function(collab) {
  587. this.requests.push(new Request.JSON({
  588. url: this.options.baseurl + collab + '/?action=' + this.options.action,
  589. data: 'args=' + encodeURIComponent(this.options.selector) + '&formatted=true',
  590. onSuccess: function(json) {
  591. if (!this.metas) this.metas = {};
  592. if (!this.formatted) this.formatted = {};
  593. this.metas[collab] = json.metas;
  594. this.formatted[collab] = json.formatted;
  595. this.construct();
  596. }.bind(this),
  597. onFailure: function(xhr) {
  598. this.empty();
  599. this.container.removeClass('waiting');
  600. this.push(new Element('tr').grab(
  601. new Element('td').set('text', xhr.responseText)
  602. ));
  603. }.bind(this)
  604. }).get());
  605. }, this);
  606. }
  607. });
  608. return {
  609. MetaTable: MetaTable,
  610. InterMetaTable: InterMetaTable
  611. };
  612. });