PageRenderTime 61ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 1ms

/lib/www/script/futon.browse.js

https://github.com/gflarity/memcouchd
JavaScript | 1296 lines | 1195 code | 76 blank | 25 comment | 280 complexity | 1227db13502889f99ad7cb897df4eb3d MD5 | raw file
Possible License(s): Apache-2.0
  1. // Licensed under the Apache License, Version 2.0 (the "License"); you may not
  2. // use this file except in compliance with the License. You may obtain a copy of
  3. // the License at
  4. //
  5. // http://www.apache.org/licenses/LICENSE-2.0
  6. //
  7. // Unless required by applicable law or agreed to in writing, software
  8. // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  9. // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  10. // License for the specific language governing permissions and limitations under
  11. // the License.
  12. (function($) {
  13. $.futon = $.futon || {};
  14. $.extend($.futon, {
  15. // Page class for browse/index.html
  16. CouchIndexPage: function() {
  17. page = this;
  18. $.futon.storage.declare("per_page", {defaultValue: 10});
  19. this.addDatabase = function() {
  20. $.showDialog("dialog/_create_database.html", {
  21. submit: function(data, callback) {
  22. if (!data.name || data.name.length == 0) {
  23. callback({name: "Please enter a name."});
  24. return;
  25. }
  26. $.couch.db(data.name).create({
  27. error: function(status, id, reason) { callback({name: reason}) },
  28. success: function(resp) {
  29. location.href = "database.html?" + encodeURIComponent(data.name);
  30. callback();
  31. }
  32. });
  33. }
  34. });
  35. return false;
  36. }
  37. this.updateDatabaseListing = function(offset) {
  38. offset |= 0;
  39. var maxPerPage = parseInt($("#perpage").val(), 10);
  40. $.couch.allDbs({
  41. success: function(dbs) {
  42. $("#paging a").unbind();
  43. $("#databases tbody.content").empty();
  44. var dbsOnPage = dbs.slice(offset, offset + maxPerPage);
  45. $.each(dbsOnPage, function(idx, dbName) {
  46. $("#databases tbody.content").append("<tr>" +
  47. "<th><a href='database.html?" + encodeURIComponent(dbName) + "'>" +
  48. dbName + "</a></th>" +
  49. "<td class='size'></td><td class='count'></td>" +
  50. "<td class='seq'></td></tr>");
  51. $.couch.db(dbName).info({
  52. success: function(info) {
  53. $("#databases tbody.content tr:eq(" + idx + ")")
  54. .find("td.size").text($.futon.formatSize(info.disk_size)).end()
  55. .find("td.count").text(info.doc_count).end()
  56. .find("td.seq").text(info.update_seq);
  57. },
  58. error : function() {}
  59. });
  60. });
  61. $("#databases tbody tr:odd").addClass("odd");
  62. if (offset > 0) {
  63. $("#paging a.prev").attr("href", "#" + (offset - maxPerPage)).click(function() {
  64. page.updateDatabaseListing(offset - maxPerPage);
  65. });
  66. } else {
  67. $("#paging a.prev").removeAttr("href");
  68. }
  69. if (offset + maxPerPage < dbs.length) {
  70. $("#paging a.next").attr("href", "#" + (offset + maxPerPage)).click(function() {
  71. page.updateDatabaseListing(offset + maxPerPage);
  72. });
  73. } else {
  74. $("#paging a.next").removeAttr("href");
  75. }
  76. var firstNum = offset + 1;
  77. var lastNum = firstNum + dbsOnPage.length - 1;
  78. $("#databases tbody.footer tr td span").text(
  79. "Showing " + firstNum + "-" + lastNum + " of " + dbs.length +
  80. " databases");
  81. }
  82. });
  83. }
  84. },
  85. // Page class for browse/database.html
  86. CouchDatabasePage: function() {
  87. var urlParts = location.search.substr(1).split("/");
  88. var dbName = decodeURIComponent(urlParts.shift())
  89. var dbNameRegExp = new RegExp("[^a-z0-9\_\$\(\)\+\/\-]", "g");
  90. dbName = dbName.replace(dbNameRegExp, "");
  91. $.futon.storage.declareWithPrefix(dbName + ".", {
  92. desc: {},
  93. language: {defaultValue: "javascript"},
  94. map_fun: {defaultValue: ""},
  95. reduce_fun: {defaultValue: ""},
  96. reduce: {},
  97. group_level: {defaultValue: 100},
  98. per_page: {defaultValue: 10},
  99. view: {defaultValue: ""},
  100. stale: {defaultValue: false}
  101. });
  102. var viewName = (urlParts.length > 0) ? urlParts.join("/") : null;
  103. if (viewName) {
  104. $.futon.storage.set("view", decodeURIComponent(viewName));
  105. } else {
  106. viewName = $.futon.storage.get("view");
  107. if (viewName) {
  108. this.redirecting = true;
  109. location.href = "database.html?" + encodeURIComponent(dbName) +
  110. "/" + encodeURIComponent(viewName);
  111. }
  112. }
  113. var db = $.couch.db(dbName);
  114. this.dbName = dbName;
  115. viewName = decodeURIComponent(viewName);
  116. this.viewName = viewName;
  117. this.viewLanguage = "javascript";
  118. this.db = db;
  119. this.isDirty = false;
  120. this.isTempView = viewName == "_temp_view";
  121. page = this;
  122. var templates = {
  123. javascript: "function(doc) {\n emit(null, doc);\n}",
  124. python: "def fun(doc):\n yield None, doc",
  125. ruby: "lambda {|doc|\n emit(nil, doc);\n}"
  126. }
  127. this.newDocument = function() {
  128. location.href = "document.html?" + encodeURIComponent(db.name);
  129. }
  130. this.compactAndCleanup = function() {
  131. $.showDialog("dialog/_compact_cleanup.html", {
  132. submit: function(data, callback) {
  133. switch (data.action) {
  134. case "compact_database":
  135. db.compact({success: function(resp) { callback() }});
  136. break;
  137. case "compact_views":
  138. var idx = page.viewName.indexOf("/_view");
  139. if (idx == -1) {
  140. alert("Compact Views requires focus on a view!");
  141. } else {
  142. var groupname = page.viewName.substring(8, idx);
  143. db.compactView(groupname, {success: function(resp) { callback() }});
  144. }
  145. break;
  146. case "view_cleanup":
  147. db.viewCleanup({success: function(resp) { callback() }});
  148. break;
  149. }
  150. }
  151. });
  152. }
  153. this.deleteDatabase = function() {
  154. $.showDialog("dialog/_delete_database.html", {
  155. submit: function(data, callback) {
  156. db.drop({
  157. success: function(resp) {
  158. callback();
  159. location.href = "index.html";
  160. if (window !== null) {
  161. $("#dbs li").filter(function(index) {
  162. return $("a", this).text() == dbName;
  163. }).remove();
  164. $.futon.navigation.removeDatabase(dbName);
  165. }
  166. }
  167. });
  168. }
  169. });
  170. }
  171. this.databaseSecurity = function() {
  172. function namesAndRoles(r, key) {
  173. var names = [];
  174. var roles = [];
  175. if (r && typeof r[key + "s"] === "object") {
  176. if ($.isArray(r[key + "s"]["names"])) {
  177. names = r[key + "s"]["names"];
  178. }
  179. if ($.isArray(r[key + "s"]["roles"])) {
  180. roles = r[key + "s"]["roles"];
  181. }
  182. }
  183. return {names : names, roles: roles};
  184. };
  185. $.showDialog("dialog/_database_security.html", {
  186. load : function(d) {
  187. db.getDbProperty("_security", {
  188. success: function(r) {
  189. var admins = namesAndRoles(r, "admin")
  190. , members = namesAndRoles(r, "member");
  191. if (members.names.length + members.roles.length == 0) {
  192. // backwards compatibility with readers for 1.x
  193. members = namesAndRoles(r, "reader");
  194. }
  195. $("input[name=admin_names]", d).val(JSON.stringify(admins.names));
  196. $("input[name=admin_roles]", d).val(JSON.stringify(admins.roles));
  197. $("input[name=member_names]", d).val(JSON.stringify(members.names));
  198. $("input[name=member_roles]", d).val(JSON.stringify(members.roles));
  199. }
  200. });
  201. },
  202. // maybe this should be 2 forms
  203. submit: function(data, callback) {
  204. var errors = {};
  205. var secObj = {
  206. admins: {
  207. names: [],
  208. roles: []
  209. },
  210. members: {
  211. names: [],
  212. roles: []
  213. }
  214. };
  215. ["admin", "member"].forEach(function(key) {
  216. var names, roles;
  217. try {
  218. names = JSON.parse(data[key + "_names"]);
  219. } catch(e) { }
  220. try {
  221. roles = JSON.parse(data[key + "_roles"]);
  222. } catch(e) { }
  223. if ($.isArray(names)) {
  224. secObj[key + "s"]["names"] = names;
  225. } else {
  226. errors[key + "_names"] = "The " + key +
  227. " names must be an array of strings";
  228. }
  229. if ($.isArray(roles)) {
  230. secObj[key + "s"]["roles"] = roles;
  231. } else {
  232. errors[key + "_roles"] = "The " + key +
  233. " roles must be an array of strings";
  234. }
  235. });
  236. if ($.isEmptyObject(errors)) {
  237. db.setDbProperty("_security", secObj);
  238. }
  239. callback(errors);
  240. }
  241. });
  242. }
  243. this.populateViewEditor = function() {
  244. if (viewName.match(/^_design\//)) {
  245. page.revertViewChanges(function() {
  246. var dirtyTimeout = null;
  247. function updateDirtyState() {
  248. clearTimeout(dirtyTimeout);
  249. dirtyTimeout = setTimeout(function() {
  250. var buttons = $("#viewcode button.save, #viewcode button.revert");
  251. var viewCode = {
  252. map: $("#viewcode_map").val(),
  253. reduce: $("#viewcode_reduce").val()
  254. };
  255. $("#reduce, #grouplevel").toggle(!!viewCode.reduce);
  256. page.isDirty = (viewCode.map != page.storedViewCode.map)
  257. || (viewCode.reduce != (page.storedViewCode.reduce || ""))
  258. || page.viewLanguage != page.storedViewLanguage;
  259. if (page.isDirty) {
  260. buttons.removeAttr("disabled");
  261. } else {
  262. buttons.attr("disabled", "disabled");
  263. }
  264. }, 100);
  265. }
  266. $("#viewcode textarea").enableTabInsertion()
  267. .bind("input", updateDirtyState);
  268. if ($.browser.msie || $.browser.safari) {
  269. $("#viewcode textarea").bind("paste", updateDirtyState)
  270. .bind("change", updateDirtyState)
  271. .bind("keydown", updateDirtyState)
  272. .bind("keypress", updateDirtyState)
  273. .bind("keyup", updateDirtyState)
  274. .bind("textInput", updateDirtyState);
  275. }
  276. $("#language").change(updateDirtyState);
  277. page.updateDocumentListing();
  278. });
  279. } else if (viewName == "_temp_view") {
  280. $("#viewcode textarea").enableTabInsertion();
  281. page.viewLanguage = $.futon.storage.get("language");
  282. page.updateViewEditor(
  283. $.futon.storage.get("map_fun", templates[page.viewLanguage]),
  284. $.futon.storage.get("reduce_fun")
  285. );
  286. } else {
  287. $("#grouplevel, #reduce").hide();
  288. page.updateDocumentListing();
  289. }
  290. page.populateLanguagesMenu();
  291. if (this.isTempView) {
  292. $("#tempwarn").show();
  293. }
  294. }
  295. // Populate the languages dropdown, and listen to selection changes
  296. this.populateLanguagesMenu = function() {
  297. var all_langs = {};
  298. fill_language = function() {
  299. var select = $("#language");
  300. for (var language in all_langs) {
  301. var option = $(document.createElement("option"))
  302. .attr("value", language).text(language)
  303. .appendTo(select);
  304. }
  305. if (select[0].options.length == 1) {
  306. select[0].disabled = true;
  307. } else {
  308. select[0].disabled = false;
  309. select.val(page.viewLanguage);
  310. select.change(function() {
  311. var language = $("#language").val();
  312. if (language != page.viewLanguage) {
  313. var mapFun = $("#viewcode_map").val();
  314. if (mapFun == "" || mapFun == templates[page.viewLanguage]) {
  315. // no edits made, so change to the new default
  316. $("#viewcode_map").val(templates[language]);
  317. }
  318. page.viewLanguage = language;
  319. $("#viewcode_map")[0].focus();
  320. }
  321. return false;
  322. });
  323. }
  324. }
  325. $.couch.config({
  326. success: function(resp) {
  327. for (var language in resp) {
  328. all_langs[language] = resp[language];
  329. }
  330. $.couch.config({
  331. success: function(resp) {
  332. for (var language in resp) {
  333. all_langs[language] = resp[language];
  334. }
  335. fill_language();
  336. }
  337. }, "native_query_servers");
  338. },
  339. error : function() {}
  340. }, "query_servers");
  341. }
  342. this.populateViewsMenu = function() {
  343. var select = $("#switch select");
  344. db.allDocs({startkey: "_design/", endkey: "_design0",
  345. include_docs: true,
  346. success: function(resp) {
  347. select[0].options.length = 3;
  348. for (var i = 0; i < resp.rows.length; i++) {
  349. var doc = resp.rows[i].doc;
  350. var optGroup = $(document.createElement("optgroup"))
  351. .attr("label", doc._id.substr(8)).appendTo(select);
  352. var viewNames = [];
  353. for (var name in doc.views) {
  354. viewNames.push(name);
  355. }
  356. viewNames.sort();
  357. for (var j = 0; j < viewNames.length; j++) {
  358. var path = $.couch.encodeDocId(doc._id) + "/_view/" +
  359. encodeURIComponent(viewNames[j]);
  360. var option = $(document.createElement("option"))
  361. .attr("value", path).text(encodeURIComponent(viewNames[j]))
  362. .appendTo(optGroup);
  363. if (path == viewName) {
  364. option[0].selected = true;
  365. }
  366. }
  367. }
  368. }
  369. });
  370. if (!viewName.match(/^_design\//)) {
  371. $.each(["_all_docs", "_design_docs", "_temp_view"], function(idx, name) {
  372. if (viewName == name) {
  373. select[0].options[idx].selected = true;
  374. }
  375. });
  376. }
  377. }
  378. this.revertViewChanges = function(callback) {
  379. if (!page.storedViewCode) {
  380. var viewNameParts = viewName.split("/");
  381. var designDocId = decodeURIComponent(viewNameParts[1]);
  382. var localViewName = decodeURIComponent(viewNameParts[3]);
  383. db.openDoc("_design/" + designDocId, {
  384. error: function(status, error, reason) {
  385. if (status == 404) {
  386. $.futon.storage.del("view");
  387. location.href = "database.html?" + encodeURIComponent(db.name);
  388. }
  389. },
  390. success: function(resp) {
  391. if(!resp.views || !resp.views[localViewName]) {
  392. $.futon.storage.del("view");
  393. location.href = "database.html?" + encodeURIComponent(db.name);
  394. }
  395. var viewCode = resp.views[localViewName];
  396. page.viewLanguage = resp.language || "javascript";
  397. $("#language").val(encodeURIComponent(page.viewLanguage));
  398. page.updateViewEditor(viewCode.map, viewCode.reduce || "");
  399. $("#viewcode button.revert, #viewcode button.save").attr("disabled", "disabled");
  400. page.storedViewCode = viewCode;
  401. page.storedViewLanguage = page.viewLanguage;
  402. if (callback) callback();
  403. }
  404. }, {async: false});
  405. } else {
  406. page.updateViewEditor(page.storedViewCode.map,
  407. page.storedViewCode.reduce || "");
  408. page.viewLanguage = page.storedViewLanguage;
  409. $("#language").val(encodeURIComponent(page.viewLanguage));
  410. $("#viewcode button.revert, #viewcode button.save").attr("disabled", "disabled");
  411. page.isDirty = false;
  412. if (callback) callback();
  413. }
  414. }
  415. this.updateViewEditor = function(mapFun, reduceFun) {
  416. if (!mapFun) return;
  417. $("#viewcode_map").val(mapFun);
  418. $("#viewcode_reduce").val(reduceFun);
  419. var lines = Math.max(
  420. mapFun.split("\n").length,
  421. reduceFun.split("\n").length
  422. );
  423. $("#reduce, #grouplevel").toggle(!!reduceFun);
  424. $("#viewcode textarea").attr("rows", Math.min(15, Math.max(3, lines)));
  425. }
  426. this.saveViewAs = function() {
  427. if (viewName && /^_design/.test(viewName)) {
  428. var viewNameParts = viewName.split("/");
  429. var designDocId = decodeURIComponent(viewNameParts[1]);
  430. var localViewName = decodeURIComponent(viewNameParts[3]);
  431. } else {
  432. var designDocId = "", localViewName = "";
  433. }
  434. $.showDialog("dialog/_save_view_as.html", {
  435. load: function(elem) {
  436. $("#input_docid", elem).val(designDocId).suggest(function(text, callback) {
  437. db.allDocs({
  438. limit: 10, startkey: "_design/" + text, endkey: "_design0",
  439. success: function(docs) {
  440. var matches = [];
  441. for (var i = 0; i < docs.rows.length; i++) {
  442. var docName = docs.rows[i].id.substr(8);
  443. if (docName.indexOf(text) == 0) {
  444. matches[i] = docName;
  445. }
  446. }
  447. callback(matches);
  448. }
  449. });
  450. });
  451. $("#input_name", elem).val(localViewName).suggest(function(text, callback) {
  452. db.openDoc("_design/" + $("#input_docid").val(), {
  453. error: function() {}, // ignore
  454. success: function(doc) {
  455. var matches = [];
  456. if (!doc.views) return;
  457. for (var viewName in doc.views) {
  458. if (viewName.indexOf(text) == 0) {
  459. matches.push(viewName);
  460. }
  461. }
  462. callback(matches);
  463. }
  464. });
  465. });
  466. },
  467. submit: function(data, callback) {
  468. if (!data.docid || !data.name) {
  469. var errors = {};
  470. if (!data.docid) errors.docid = "Please enter a document ID";
  471. if (!data.name) errors.name = "Please enter a view name";
  472. callback(errors);
  473. } else {
  474. var viewCode = {
  475. map: $("#viewcode_map").val(),
  476. reduce: $("#viewcode_reduce").val() || undefined
  477. };
  478. var docId = ["_design", data.docid].join("/");
  479. function save(doc) {
  480. if (!doc) {
  481. doc = {_id: docId, language: page.viewLanguage};
  482. } else {
  483. var numViews = 0;
  484. for (var viewName in (doc.views || {})) {
  485. if (viewName != data.name) numViews++;
  486. }
  487. if (numViews > 0 && page.viewLanguage != doc.language) {
  488. callback({
  489. docid: "Cannot save to " + data.docid +
  490. " because its language is \"" + doc.language +
  491. "\", not \"" +
  492. encodeURIComponent(page.viewLanguage) + "\"."
  493. });
  494. return;
  495. }
  496. doc.language = page.viewLanguage;
  497. }
  498. if (doc.views === undefined) doc.views = {};
  499. doc.views[data.name] = viewCode;
  500. db.saveDoc(doc, {
  501. success: function(resp) {
  502. callback();
  503. page.isDirty = false;
  504. location.href = "database.html?" + encodeURIComponent(dbName) +
  505. "/" + $.couch.encodeDocId(doc._id) +
  506. "/_view/" + encodeURIComponent(data.name);
  507. }
  508. });
  509. }
  510. db.openDoc(docId, {
  511. error: function(status, error, reason) {
  512. if (status == 404) save(null);
  513. else alert(reason);
  514. },
  515. success: function(doc) {
  516. save(doc);
  517. }
  518. });
  519. }
  520. }
  521. });
  522. }
  523. this.saveViewChanges = function() {
  524. var viewNameParts = viewName.split("/");
  525. var designDocId = decodeURIComponent(viewNameParts[1]);
  526. var localViewName = decodeURIComponent(viewNameParts[3]);
  527. db.openDoc("_design/" + designDocId, {
  528. success: function(doc) {
  529. var numViews = 0;
  530. for (var viewName in (doc.views || {})) {
  531. if (viewName != localViewName) numViews++;
  532. }
  533. if (numViews > 0 && page.viewLanguage != doc.language) {
  534. alert("Cannot save view because the design document language " +
  535. "is \"" + doc.language + "\", not \"" +
  536. page.viewLanguage + "\".");
  537. return;
  538. }
  539. doc.language = page.viewLanguage;
  540. var viewDef = doc.views[localViewName];
  541. viewDef.map = $("#viewcode_map").val();
  542. viewDef.reduce = $("#viewcode_reduce").val() || undefined;
  543. db.saveDoc(doc, {
  544. success: function(resp) {
  545. page.isDirty = false;
  546. $("#viewcode button.revert, #viewcode button.save")
  547. .attr("disabled", "disabled");
  548. }
  549. });
  550. }
  551. });
  552. }
  553. this.updateDesignDocLink = function() {
  554. if (viewName && /^_design/.test(viewName)) {
  555. var docId = "_design/" + encodeURIComponent(decodeURIComponent(viewName).split("/")[1]);
  556. $("#designdoc-link").attr("href", "document.html?" +
  557. encodeURIComponent(dbName) + "/" + $.couch.encodeDocId(docId)).text(docId);
  558. } else {
  559. $("#designdoc-link").removeAttr("href").text("");
  560. }
  561. }
  562. this.jumpToDocument = function(docId) {
  563. if (docId != "") {
  564. location.href = 'document.html?' + encodeURIComponent(db.name)
  565. + "/" + $.couch.encodeDocId(docId);
  566. }
  567. }
  568. this.updateDocumentListing = function(options) {
  569. if (options === undefined) options = {};
  570. if (options.limit === undefined) {
  571. var perPage = parseInt($("#perpage").val(), 10)
  572. // Fetch an extra row so we know when we're on the last page for
  573. // reduce views
  574. options.limit = perPage + 1;
  575. } else {
  576. perPage = options.limit - 1;
  577. }
  578. if ($("#documents thead th.key").is(".desc")) {
  579. if (typeof options.descending == 'undefined') options.descending = true;
  580. var descend = true;
  581. $.futon.storage.set("desc", "1");
  582. } else {
  583. var descend = false;
  584. $.futon.storage.del("desc");
  585. }
  586. $("#paging a").unbind();
  587. $("#documents").find("tbody.content").empty().end().show();
  588. page.updateDesignDocLink();
  589. options.success = function(resp) {
  590. if (resp.offset === undefined) {
  591. resp.offset = 0;
  592. }
  593. var descending_reverse = ((options.descending && !descend) || (descend && (options.descending === false)));
  594. var has_reduce_prev = resp.total_rows === undefined && (descending_reverse ? resp.rows.length > perPage : options.startkey !== undefined);
  595. if (descending_reverse && resp.rows) {
  596. resp.rows = resp.rows.reverse();
  597. if (resp.rows.length > perPage) {
  598. resp.rows.push(resp.rows.shift());
  599. }
  600. }
  601. if (resp.rows !== null && (has_reduce_prev || (descending_reverse ?
  602. (resp.total_rows - resp.offset > perPage) :
  603. (resp.offset > 0)))) {
  604. $("#paging a.prev").attr("href", "#" + (resp.offset - perPage)).click(function() {
  605. var opt = {
  606. descending: !descend,
  607. limit: options.limit
  608. };
  609. if (resp.rows.length > 0) {
  610. var firstDoc = resp.rows[0];
  611. opt.startkey = firstDoc.key !== undefined ? firstDoc.key : null;
  612. if (firstDoc.id !== undefined) {
  613. opt.startkey_docid = firstDoc.id;
  614. }
  615. opt.skip = 1;
  616. }
  617. page.updateDocumentListing(opt);
  618. return false;
  619. });
  620. } else {
  621. $("#paging a.prev").removeAttr("href");
  622. }
  623. var has_reduce_next = resp.total_rows === undefined && (descending_reverse ? options.startkey !== undefined : resp.rows.length > perPage);
  624. if (resp.rows !== null && (has_reduce_next || (descending_reverse ?
  625. (resp.offset - resp.total_rows < perPage) :
  626. (resp.total_rows - resp.offset > perPage)))) {
  627. $("#paging a.next").attr("href", "#" + (resp.offset + perPage)).click(function() {
  628. var opt = {
  629. descending: descend,
  630. limit: options.limit
  631. };
  632. if (resp.rows.length > 0) {
  633. var lastDoc = resp.rows[Math.min(perPage, resp.rows.length) - 1];
  634. opt.startkey = lastDoc.key !== undefined ? lastDoc.key : null;
  635. if (lastDoc.id !== undefined) {
  636. opt.startkey_docid = lastDoc.id;
  637. }
  638. opt.skip = 1;
  639. }
  640. page.updateDocumentListing(opt);
  641. return false;
  642. });
  643. } else {
  644. $("#paging a.next").removeAttr("href");
  645. }
  646. for (var i = 0; i < Math.min(perPage, resp.rows.length); i++) {
  647. var row = resp.rows[i];
  648. var tr = $("<tr></tr>");
  649. var key = "null";
  650. if (row.key !== null) {
  651. key = $.futon.formatJSON(row.key, {indent: 0, linesep: ""});
  652. }
  653. if (row.id) {
  654. $("<td class='key'><a href='document.html?" + encodeURIComponent(db.name) +
  655. "/" + $.couch.encodeDocId(row.id) + "'><strong></strong><br>" +
  656. "<span class='docid'>ID:&nbsp;" + $.futon.escape(row.id) + "</span></a></td>")
  657. .find("strong").text(key).end()
  658. .appendTo(tr);
  659. } else {
  660. $("<td class='key'><strong></strong></td>")
  661. .find("strong").text(key).end()
  662. .appendTo(tr);
  663. }
  664. var value = "null";
  665. if (row.value !== null) {
  666. value = $.futon.formatJSON(row.value, {
  667. html: true, indent: 0, linesep: "", quoteKeys: false
  668. });
  669. }
  670. $("<td class='value'><div></div></td>").find("div").html(value).end()
  671. .appendTo(tr).dblclick(function() {
  672. location.href = this.previousSibling.firstChild.href;
  673. });
  674. tr.appendTo("#documents tbody.content");
  675. }
  676. var firstNum = 1;
  677. var lastNum = totalNum = Math.min(perPage, resp.rows.length);
  678. if (resp.total_rows != null) {
  679. if (descending_reverse) {
  680. lastNum = Math.min(resp.total_rows, resp.total_rows - resp.offset);
  681. firstNum = lastNum - totalNum + 1;
  682. } else {
  683. firstNum = Math.min(resp.total_rows, resp.offset + 1);
  684. lastNum = firstNum + totalNum - 1;
  685. }
  686. totalNum = resp.total_rows;
  687. } else {
  688. totalNum = "unknown";
  689. }
  690. $("#paging").show();
  691. $("#documents tbody.footer td span").text(
  692. "Showing " + firstNum + "-" + lastNum + " of " + totalNum +
  693. " row" + (firstNum != lastNum || totalNum == "unknown" ? "s" : ""));
  694. $("#documents tbody tr:odd").addClass("odd");
  695. }
  696. options.error = function(status, error, reason) {
  697. alert("Error: " + error + "\n\n" + reason);
  698. }
  699. if (!viewName || viewName == "_all_docs") {
  700. $("#switch select")[0].selectedIndex = 0;
  701. db.allDocs(options);
  702. } else {
  703. if (viewName == "_temp_view") {
  704. $("#viewcode").show().removeClass("collapsed");
  705. var mapFun = $("#viewcode_map").val();
  706. $.futon.storage.set("map_fun", mapFun);
  707. var reduceFun = $.trim($("#viewcode_reduce").val()) || null;
  708. if (reduceFun) {
  709. $.futon.storage.set("reduce_fun", reduceFun);
  710. if ($("#reduce :checked").length) {
  711. var level = parseInt($("#grouplevel select").val(), 10);
  712. options.group = level > 0;
  713. if (options.group && level < 100) {
  714. options.group_level = level;
  715. }
  716. } else {
  717. options.reduce = false;
  718. }
  719. }
  720. $.futon.storage.set("language", page.viewLanguage);
  721. db.query(mapFun, reduceFun, page.viewLanguage, options);
  722. } else if (viewName == "_design_docs") {
  723. options.startkey = options.descending ? "_design0" : "_design";
  724. options.endkey = options.descending ? "_design" : "_design0";
  725. db.allDocs(options);
  726. } else {
  727. $("button.compactview").show();
  728. $("#viewcode").show();
  729. var currentMapCode = $("#viewcode_map").val();
  730. var currentReduceCode = $.trim($("#viewcode_reduce").val()) || null;
  731. if (currentReduceCode) {
  732. if ($("#reduce :checked").length) {
  733. var level = parseInt($("#grouplevel select").val(), 10);
  734. options.group = level > 0;
  735. if (options.group && level < 100) {
  736. options.group_level = level;
  737. }
  738. } else {
  739. options.reduce = false;
  740. }
  741. }
  742. if (page.isDirty) {
  743. db.query(currentMapCode, currentReduceCode, page.viewLanguage, options);
  744. } else {
  745. var viewParts = decodeURIComponent(viewName).split('/');
  746. if ($.futon.storage.get("stale")) {
  747. options.stale = "ok";
  748. }
  749. db.view(viewParts[1] + "/" + viewParts[3], options);
  750. }
  751. }
  752. }
  753. }
  754. window.onbeforeunload = function() {
  755. $("#switch select").val(viewName);
  756. if (page.isDirty) {
  757. return "You've made changes to the view code that have not been " +
  758. "saved yet.";
  759. }
  760. }
  761. },
  762. // Page class for browse/document.html
  763. CouchDocumentPage: function() {
  764. var urlParts = location.search.substr(1).split("/");
  765. var dbName = decodeURIComponent(urlParts.shift());
  766. if (urlParts.length) {
  767. var idParts = urlParts.join("/").split("@", 2);
  768. var docId = decodeURIComponent(idParts[0]);
  769. var docRev = (idParts.length > 1) ? idParts[1] : null;
  770. this.isNew = false;
  771. } else {
  772. var docId = $.couch.newUUID();
  773. var docRev = null;
  774. this.isNew = true;
  775. }
  776. var db = $.couch.db(dbName);
  777. $.futon.storage.declare("tab", {defaultValue: "tabular", scope: "cookie"});
  778. this.dbName = dbName;
  779. this.db = db;
  780. this.docId = docId;
  781. this.doc = null;
  782. this.isDirty = this.isNew;
  783. page = this;
  784. this.activateTabularView = function() {
  785. if ($("#fields tbody.source textarea").length > 0)
  786. return;
  787. $.futon.storage.set("tab", "tabular");
  788. $("#tabs li").removeClass("active").filter(".tabular").addClass("active");
  789. $("#fields thead th:first").text("Field").attr("colspan", 1).next().show();
  790. $("#fields tbody.content").show();
  791. $("#fields tbody.source").hide();
  792. return false;
  793. }
  794. this.activateSourceView = function() {
  795. $.futon.storage.set("tab", "source");
  796. $("#tabs li").removeClass("active").filter(".source").addClass("active");
  797. $("#fields thead th:first").text("Source").attr("colspan", 2).next().hide();
  798. $("#fields tbody.content").hide();
  799. $("#fields tbody.source").find("td").each(function() {
  800. $(this).html($("<pre></pre>").html($.futon.formatJSON(page.doc, {html: true})))
  801. .makeEditable({allowEmpty: false,
  802. createInput: function(value) {
  803. var rows = value.split("\n").length;
  804. return $("<textarea rows='" + rows + "' cols='80' spellcheck='false'></textarea>").enableTabInsertion();
  805. },
  806. prepareInput: function(input) {
  807. $(input).makeResizable({vertical: true});
  808. },
  809. end: function() {
  810. $(this).html($("<pre></pre>").html($.futon.formatJSON(page.doc, {html: true})));
  811. },
  812. accept: function(newValue) {
  813. page.doc = JSON.parse(newValue);
  814. page.isDirty = true;
  815. page.updateFieldListing(true);
  816. },
  817. populate: function(value) {
  818. return $.futon.formatJSON(page.doc);
  819. },
  820. validate: function(value) {
  821. try {
  822. var doc = JSON.parse(value);
  823. if (typeof doc != "object")
  824. throw new SyntaxError("Please enter a valid JSON document (for example, {}).");
  825. return true;
  826. } catch (err) {
  827. var msg = err.message;
  828. if (msg == "parseJSON" || msg == "JSON.parse") {
  829. msg = "There is a syntax error in the document.";
  830. }
  831. $("<div class='error'></div>").text(msg).appendTo(this);
  832. return false;
  833. }
  834. }
  835. });
  836. }).end().show();
  837. return false;
  838. }
  839. this.addField = function() {
  840. if (!$("#fields tbody.content:visible").length) {
  841. location.hash = "#tabular";
  842. page.activateTabularView();
  843. }
  844. var fieldName = "unnamed";
  845. var fieldIdx = 1;
  846. while (page.doc.hasOwnProperty(fieldName)) {
  847. fieldName = "unnamed " + fieldIdx++;
  848. }
  849. page.doc[fieldName] = null;
  850. var row = _addRowForField(page.doc, fieldName);
  851. page.isDirty = true;
  852. row.find("th b").dblclick();
  853. }
  854. var _sortFields = function(a, b) {
  855. var a0 = a.charAt(0), b0 = b.charAt(0);
  856. if (a0 == "_" && b0 != "_") {
  857. return -1;
  858. } else if (a0 != "_" && b0 == "_") {
  859. return 1;
  860. } else if (a == "_attachments" || b == "_attachments") {
  861. return a0 == "_attachments" ? 1 : -1;
  862. } else {
  863. return a < b ? -1 : a != b ? 1 : 0;
  864. }
  865. }
  866. this.updateFieldListing = function(noReload) {
  867. $("#fields tbody.content").empty();
  868. function handleResult(doc, revs) {
  869. page.doc = doc;
  870. var propNames = [];
  871. for (var prop in doc) {
  872. propNames.push(prop);
  873. }
  874. // Order properties alphabetically, but put internal fields first
  875. propNames.sort(_sortFields);
  876. for (var pi = 0; pi < propNames.length; pi++) {
  877. _addRowForField(doc, propNames[pi]);
  878. }
  879. if (revs.length > 1) {
  880. var currentIndex = 0;
  881. for (var i = 0; i < revs.length; i++) {
  882. if (revs[i].rev == doc._rev) {
  883. currentIndex = i;
  884. break;
  885. }
  886. }
  887. if (currentIndex < revs.length - 1) {
  888. var prevRev = revs[currentIndex + 1].rev;
  889. $("#paging a.prev").attr("href", "?" + encodeURIComponent(dbName) +
  890. "/" + $.couch.encodeDocId(docId) + "@" + prevRev);
  891. }
  892. if (currentIndex > 0) {
  893. var nextRev = revs[currentIndex - 1].rev;
  894. $("#paging a.next").attr("href", "?" + encodeURIComponent(dbName) +
  895. "/" + $.couch.encodeDocId(docId) + "@" + nextRev);
  896. }
  897. $("#fields tbody.footer td span").text("Showing revision " +
  898. (revs.length - currentIndex) + " of " + revs.length);
  899. }
  900. if ($.futon.storage.get("tab") == "source") {
  901. page.activateSourceView();
  902. }
  903. }
  904. if (noReload) {
  905. handleResult(page.doc, []);
  906. return;
  907. }
  908. if (!page.isNew) {
  909. db.openDoc(docId, {revs_info: true,
  910. success: function(doc) {
  911. var revs = doc._revs_info || [];
  912. delete doc._revs_info;
  913. if (docRev != null) {
  914. db.openDoc(docId, {rev: docRev,
  915. error: function(status, error, reason) {
  916. alert("The requested revision was not found. You will " +
  917. "be redirected back to the latest revision.");
  918. location.href = "?" + encodeURIComponent(dbName) +
  919. "/" + $.couch.encodeDocId(docId);
  920. },
  921. success: function(doc) {
  922. handleResult(doc, revs);
  923. }
  924. });
  925. } else {
  926. handleResult(doc, revs);
  927. }
  928. }
  929. });
  930. } else {
  931. handleResult({_id: docId}, []);
  932. $("#fields tbody td").dblclick();
  933. }
  934. }
  935. this.deleteDocument = function() {
  936. $.showDialog("dialog/_delete_document.html", {
  937. submit: function(data, callback) {
  938. db.removeDoc(page.doc, {
  939. success: function(resp) {
  940. callback();
  941. location.href = "database.html?" + encodeURIComponent(dbName);
  942. }
  943. });
  944. }
  945. });
  946. }
  947. this.saveDocument = function() {
  948. db.saveDoc(page.doc, {
  949. error: function(status, error, reason) {
  950. alert("Error: " + error + "\n\n" + reason);
  951. },
  952. success: function(resp) {
  953. page.isDirty = false;
  954. location.href = "?" + encodeURIComponent(dbName) +
  955. "/" + $.couch.encodeDocId(page.docId);
  956. }
  957. });
  958. }
  959. this.uploadAttachment = function() {
  960. if (page.isDirty) {
  961. alert("You need to save or revert any changes you have made to the " +
  962. "document before you can attach a new file.");
  963. return false;
  964. }
  965. $.showDialog("dialog/_upload_attachment.html", {
  966. load: function(elem) {
  967. $("input[name='_rev']", elem).val(page.doc._rev);
  968. },
  969. submit: function(data, callback) {
  970. if (!data._attachments || data._attachments.length == 0) {
  971. callback({_attachments: "Please select a file to upload."});
  972. return;
  973. }
  974. var form = $("#upload-form");
  975. form.find("#progress").css("visibility", "visible");
  976. form.ajaxSubmit({
  977. url: db.uri + $.couch.encodeDocId(page.docId),
  978. success: function(resp) {
  979. form.find("#progress").css("visibility", "hidden");
  980. page.isDirty = false;
  981. location.href = "?" + encodeURIComponent(dbName) +
  982. "/" + $.couch.encodeDocId(page.docId);
  983. }
  984. });
  985. }
  986. });
  987. }
  988. window.onbeforeunload = function() {
  989. if (page.isDirty) {
  990. return "You've made changes to this document that have not been " +
  991. "saved yet.";
  992. }
  993. }
  994. function _addRowForField(doc, fieldName) {
  995. var row = $("<tr><th></th><td></td></tr>")
  996. .find("th").append($("<b></b>").text(fieldName)).end()
  997. .appendTo("#fields tbody.content");
  998. if (fieldName == "_attachments") {
  999. row.find("td").append(_renderAttachmentList(doc[fieldName]));
  1000. } else {
  1001. row.find("td").append(_renderValue(doc[fieldName]));
  1002. _initKey(doc, row, fieldName);
  1003. _initValue(doc, row, fieldName);
  1004. }
  1005. $("#fields tbody.content tr").removeClass("odd").filter(":odd").addClass("odd");
  1006. row.data("name", fieldName);
  1007. return row;
  1008. }
  1009. function _initKey(doc, row, fieldName) {
  1010. if (fieldName == "_id" || fieldName == "_rev") {
  1011. return;
  1012. }
  1013. var cell = row.find("th");
  1014. $("<button type='button' class='delete' title='Delete field'></button>").click(function() {
  1015. delete doc[fieldName];
  1016. row.remove();
  1017. page.isDirty = true;
  1018. $("#fields tbody.content tr").removeClass("odd").filter(":odd").addClass("odd");
  1019. }).prependTo(cell);
  1020. cell.find("b").makeEditable({allowEmpty: false,
  1021. accept: function(newName, oldName) {
  1022. doc[newName] = doc[oldName];
  1023. delete doc[oldName];
  1024. row.data("name", newName);
  1025. $(this).text(newName);
  1026. page.isDirty = true;
  1027. },
  1028. begin: function() {
  1029. row.find("th button.delete").hide();
  1030. return true;
  1031. },
  1032. end: function(keyCode) {
  1033. row.find("th button.delete").show();
  1034. if (keyCode == 9) { // tab, move to editing the value
  1035. row.find("td").dblclick();
  1036. }
  1037. },
  1038. validate: function(newName, oldName) {
  1039. $("div.error", this).remove();
  1040. if (newName != oldName && doc[newName] !== undefined) {
  1041. $("<div class='error'>Already have field with that name.</div>")
  1042. .appendTo(this);
  1043. return false;
  1044. }
  1045. return true;
  1046. }
  1047. });
  1048. }
  1049. function _initValue(doc, row, fieldName) {
  1050. if ((fieldName == "_id" && !page.isNew) || fieldName == "_rev") {
  1051. return;
  1052. }
  1053. row.find("td").makeEditable({acceptOnBlur: false, allowEmpty: true,
  1054. createInput: function(value) {
  1055. value = doc[row.data("name")];
  1056. var elem = $(this);
  1057. if (elem.find("dl").length > 0 ||
  1058. elem.find("code").is(".array, .object") ||
  1059. typeof(value) == "string" && (value.length > 60 || value.match(/\n/))) {
  1060. return $("<textarea rows='1' cols='40' spellcheck='false'></textarea>");
  1061. }
  1062. return $("<input type='text' spellcheck='false'>");
  1063. },
  1064. end: function() {
  1065. $(this).children().remove();
  1066. $(this).append(_renderValue(doc[row.data("name")]));
  1067. },
  1068. prepareInput: function(input) {
  1069. if ($(input).is("textarea")) {
  1070. var height = Math.min(input.scrollHeight, document.body.clientHeight - 100);
  1071. $(input).height(height).makeResizable({vertical: true}).enableTabInsertion();
  1072. }
  1073. },
  1074. accept: function(newValue) {
  1075. var fieldName = row.data("name");
  1076. try {
  1077. doc[fieldName] = JSON.parse(newValue);
  1078. } catch (err) {
  1079. doc[fieldName] = newValue;
  1080. }
  1081. page.isDirty = true;
  1082. if (fieldName == "_id") {
  1083. page.docId = page.doc._id = doc[fieldName];
  1084. $("h1 strong").text(page.docId);
  1085. }
  1086. },
  1087. populate: function(value) {
  1088. value = doc[row.data("name")];
  1089. if (typeof(value) == "string") {
  1090. return value;
  1091. }
  1092. return $.futon.formatJSON(value);
  1093. },
  1094. validate: function(value) {
  1095. $("div.error", this).remove();
  1096. try {
  1097. var parsed = JSON.parse(value);
  1098. if (row.data("name") == "_id" && typeof(parsed) != "string") {
  1099. $("<div class='error'>The document ID must be a string.</div>")
  1100. .appendTo(this);
  1101. return false;
  1102. }
  1103. return true;
  1104. } catch (err) {
  1105. return true;
  1106. }
  1107. }
  1108. });
  1109. }
  1110. function _renderValue(value) {
  1111. function isNullOrEmpty(val) {
  1112. if (val == null) return true;
  1113. for (var i in val) return false;
  1114. return true;
  1115. }
  1116. function render(val) {
  1117. var type = typeof(val);
  1118. if (type == "object" && !isNullOrEmpty(val)) {
  1119. var list = $("<dl></dl>");
  1120. for (var i in val) {
  1121. $("<dt></dt>").text(i).appendTo(list);
  1122. $("<dd></dd>").append(render(val[i])).appendTo(list);
  1123. }
  1124. return list;
  1125. } else {
  1126. var html = $.futon.formatJSON(val, {
  1127. html: true,
  1128. escapeStrings: false
  1129. });
  1130. var n = $(html);
  1131. if (n.text().length > 140) {
  1132. // This code reduces a long string in to a summarized string with a link to expand it.
  1133. // Someone, somewhere, is doing something nasty with the event after it leaves these handlers.
  1134. // At this time I can't track down the offender, it might actually be a jQuery propogation issue.
  1135. var fulltext = n.text();
  1136. var mintext = n.text().slice(0, 140);
  1137. var e = $('<a href="#expand">...</a>');
  1138. var m = $('<a href="#min">X</a>');
  1139. var expand = function (evt) {
  1140. n.empty();
  1141. n.text(fulltext);
  1142. n.append(m);
  1143. evt.stopPropagation();
  1144. evt.stopImmediatePropagation();
  1145. evt.preventDefault();
  1146. }
  1147. var minimize = function (evt) {
  1148. n.empty();
  1149. n.text(mintext);
  1150. // For some reason the old element's handler won't fire after removed and added again.
  1151. e = $('<a href="#expand">...</a>');
  1152. e.click(expand);
  1153. n.append(e);
  1154. evt.stopPropagation();
  1155. evt.stopImmediatePropagation();
  1156. evt.preventDefault();
  1157. }
  1158. e.click(expand);
  1159. n.click(minimize);
  1160. n.text(mintext);
  1161. n.append(e)
  1162. }
  1163. return n;
  1164. }
  1165. }
  1166. var elem = render(value);
  1167. elem.find("dd:has(dl)").hide().prev("dt").addClass("collapsed");
  1168. elem.find("dd:not(:has(dl))").addClass("inline").prev().addClass("inline");
  1169. elem.find("dt.collapsed").click(function() {
  1170. $(this).toggleClass("collapsed").next().toggle();
  1171. });
  1172. return elem;
  1173. }
  1174. function _renderAttachmentList(attachments) {
  1175. var ul = $("<ul></ul>").addClass("attachments");
  1176. $.each(attachments, function(idx, attachment) {
  1177. _renderAttachmentItem(idx, attachment).appendTo(ul);
  1178. });
  1179. return ul;
  1180. }
  1181. function _renderAttachmentItem(name, attachment) {
  1182. var attachmentHref = db.uri + $.couch.encodeDocId(page.docId)
  1183. + "/" + encodeAttachment(name);
  1184. var li = $("<li></li>");
  1185. $("<a href='' title='Download file' target='_top'></a>").text(name)
  1186. .attr("href", attachmentHref)
  1187. .wrapInner("<tt></tt>").appendTo(li);
  1188. $("<span>()</span>").text("" + $.futon.formatSize(attachment.length) +
  1189. ", " + attachment.content_type).addClass("info").appendTo(li);
  1190. if (name == "tests.js") {
  1191. li.find('span.info').append(', <a href="/_utils/couch_tests.html?'
  1192. + attachmentHref + '">open in test runner</a>');
  1193. }
  1194. _initAttachmentItem(name, attachment, li);
  1195. return li;
  1196. }
  1197. function _initAttachmentItem(name, attachment, li) {
  1198. $("<button type='button' class='delete' title='Delete attachment'></button>").click(function() {
  1199. if (!li.siblings("li").length) {
  1200. delete page.doc._attachments;
  1201. li.parents("tr").remove();
  1202. $("#fields tbody.content tr").removeClass("odd").filter(":odd").addClass("odd");
  1203. } else {
  1204. delete page.doc._attachments[name];
  1205. li.remove();
  1206. }
  1207. page.isDirty = true;
  1208. return false;
  1209. }).prependTo($("a", li));
  1210. }
  1211. },
  1212. });
  1213. function encodeAttachment(name) {
  1214. var encoded = [], parts = name.split('/');
  1215. for (var i=0; i < parts.length; i++) {
  1216. encoded.push(encodeURIComponent(parts[i]));
  1217. };
  1218. return encoded.join('%2f');
  1219. }
  1220. })(jQuery);