rule-scripts.html 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610
  1. <!-- Enables dragging on mobile
  2. https://github.com/Bernardo-Castilho/dragdroptouch -->
  3. <script src="/assets/DragDropTouch.js"></script>
  4. <script>
  5. // Enter JavaScript-land!
  6. // First, some functions, followed by initialization commands
  7. // Begin BUILDER functions
  8. // source: https://www.codecanal.com/html5-drag-and-copy/
  9. function allowDrop(ev) {
  10. ev.preventDefault();
  11. }
  12. function drag(ev) {
  13. ev.dataTransfer.setData("text", ev.target.id);
  14. document.getElementById("module-input").classList.add('drag-target');
  15. }
  16. function dragEnd(ev) {
  17. document.getElementById("module-input").classList.remove('drag-target');
  18. }
  19. function drop(ev) {
  20. ev.preventDefault();
  21. var target = ev.target;
  22. // First, confirm target location is valid
  23. function targetCheck () {
  24. if (!editMode) {
  25. return false;
  26. } else if (target.id == "module-input") {
  27. return true;
  28. } else if (!document.getElementById("module-input").contains(target)) {
  29. // Ignore destinations not in the correct area
  30. return false;
  31. } else if (target.id == "drag-directions") {
  32. // Prevents dropping into dummy text field
  33. target = target.parentElement;
  34. return true;
  35. } else if (target.classList[0] == "module") {
  36. return true;
  37. } else {
  38. // be sure we're adding to module, not its children
  39. target = target.parentElement;
  40. return targetCheck();
  41. }
  42. }
  43. if (!targetCheck()) { return; }
  44. // Set up transfer
  45. var data = ev.dataTransfer.getData("text");
  46. // Iff module is from the menu clone it
  47. var module = document.getElementById(data);
  48. if ((module.parentElement.id == "module-menu") ||
  49. (module.parentElement.parentElement.id == "module-menu")) {
  50. // ^ because a subgroup might be parent
  51. module = module.cloneNode(true);
  52. var name = null;
  53. if (module.id == "module-custom") {
  54. // For custom modules: replace the <input> with text
  55. name = module.getElementsByTagName("input")[0].value;
  56. module.getElementsByTagName("input")[0].remove();
  57. var customText = document.createElement("span");
  58. customText.id = "module-name";
  59. customText.append(name);
  60. module.prepend(customText);
  61. }
  62. // append id with unique timestamp
  63. var nowModule = new Date();
  64. module.id += "-" + nowModule.getTime();
  65. }
  66. // hide the info button
  67. // module.children[1].style.display = "none";
  68. // display the deletion button
  69. // module.children[2].style.display = "inline";
  70. // pop it in!
  71. target.appendChild(module);
  72. // add module-field button (using HTML so it saves to Library)
  73. module.outerHTML = module.outerHTML.replace("id=\"module-name\"",
  74. "id=\"module-name\" onclick=\"moduleEditField(this.parentNode.id)\"");
  75. // create the module-field
  76. moduleEditField(module.id);
  77. // be sure the dummy text is gone
  78. if (document.contains(document.getElementById("drag-directions"))) {
  79. document.getElementById("drag-directions").remove();
  80. }
  81. }
  82. // Edits the title field of a given module based on #custom-field
  83. function moduleTitleEdit(moduleID) {
  84. var module = document.getElementById(moduleID).children[0];
  85. var content =
  86. stripHTML(document.getElementById("custom-field").innerHTML);
  87. module.title = content;
  88. }
  89. // Sets up a field for displaying and editing module details
  90. function moduleEditField(moduleID) {
  91. var module = document.getElementById(moduleID);
  92. var moduleName = module.children[0].innerHTML;
  93. var moduleTitle = module.children[0].title;
  94. if (editMode) {
  95. var query = "How does the <strong>" + moduleName
  96. + "</strong> module work?";
  97. var destination = document.getElementById("builder-field");
  98. if (moduleName == null) { moduleName = ""; }
  99. var output = '\n<div id="custom-field-container">';
  100. output += '<span class="prompt">' + query + '</span>';
  101. output += '<div class="field-controls"><a onclick="this.parentNode.parentNode.remove()"><img src="{% link assets/tabler_icons/x.svg %}" class="delete-module" /></a></div>';
  102. output += '<p contenteditable="true" class="editable" id="custom-field" oninput="moduleTitleEdit(\'' + moduleID + '\')">' + moduleTitle + '</p>';
  103. output += '</div>\n';
  104. destination.innerHTML = output;
  105. } else {
  106. var output = '\n<div id="custom-field-container">';
  107. output += '<div class="field-controls"><a onclick="this.parentNode.parentNode.remove()"><img src="{% link assets/tabler_icons/x.svg %}" class="delete-module" /></a></div>';
  108. output += '<p class="editable" id="custom-field">'
  109. + moduleTitle + '</p>';
  110. }
  111. }
  112. // Tests if the RuleBuilder is empty
  113. function builderEmpty() {
  114. var builder = document.getElementById("module-input");
  115. var childs = builder.children;
  116. if (builder.getElementsByClassName("module").length > 0) {
  117. return false;
  118. } else {
  119. return true;
  120. }
  121. }
  122. // Turns RuleBuilder contents into an output-ready nested array
  123. // Returns empty array if no modules
  124. function builderArray() {
  125. var modules = document.getElementById("module-input").children;
  126. // takes an array of children
  127. // returns an array with all modules in the array, recursively nested
  128. function iterateArray (childs) {
  129. var moduleArray = [];
  130. if (childs.length > 0) {
  131. for (var i = 0; i < childs.length; i++) {
  132. module = childs[i];
  133. if (module.classList[0] == "module") {
  134. var moduleName = module.children.item("module-name");
  135. var moduleData = moduleName.title;
  136. var moduleChilds = module.children;
  137. moduleArray.push(
  138. [stripHTML(moduleName.innerHTML),
  139. stripHTML(moduleData),
  140. iterateArray(moduleChilds)]);
  141. }
  142. }
  143. }
  144. return moduleArray;
  145. } // end function
  146. return iterateArray(modules);
  147. }
  148. // returns HTML version of Builder content
  149. function displayBuilderHTML() {
  150. var output = "";
  151. var mainArray = builderArray();
  152. function arrayHTML(thisArray) {
  153. var thisOutput = "";
  154. if (thisArray.length > 0) {
  155. thisOutput += '<ul>\n';
  156. for (var i = 0; i < thisArray.length; i++) {
  157. var item = thisArray[i];
  158. thisOutput += '<li><strong>' + item[0] + '</strong> ';
  159. thisOutput += item[1] + '</li>\n';
  160. if (item[2].length > 0) {
  161. thisOutput += arrayHTML(item[2]);
  162. }
  163. }
  164. thisOutput += '</ul>\n';
  165. }
  166. return thisOutput
  167. }
  168. if (mainArray.length > 0) {
  169. output += '<div class="builder-list">\n';
  170. output += arrayHTML(mainArray) + '\n</div>\n';
  171. }
  172. return output;
  173. }
  174. // returns Markdown version of Builder content
  175. function displayBuilderMD() {
  176. var mainArray = builderArray();
  177. var indentLevel = 0;
  178. function arrayMD(thisArray) {
  179. var thisOutput = "";
  180. if (thisArray.length > 0) {
  181. for (var i = 0; i < thisArray.length; i++) {
  182. var item = thisArray[i];
  183. for (var x = 0; x < indentLevel; x++) {
  184. thisOutput += " ";
  185. }
  186. thisOutput += "* **" + item[0] + "** ";
  187. thisOutput += item[1] + "\n";
  188. if (item[2].length > 0) {
  189. indentLevel++;
  190. thisOutput += arrayMD(item[2]);
  191. indentLevel--;
  192. }
  193. }
  194. }
  195. return thisOutput;
  196. }
  197. return arrayMD(mainArray);
  198. }
  199. // end RuleBuilder functions
  200. // Removes all HTML content, replacing line break tags with newlines
  201. function stripHTML(input) {
  202. input = input.replace(/<br ?\/?>/ig, "\n").replace(/(<([^>]+)>)/ig,'');
  203. return input;
  204. }
  205. // Intercepts the paste event for editable fields and
  206. // converts the pasted content to plain text,
  207. // stripping styles and unwanted markup added by programs like Word.
  208. function handleEditablePaste(event) {
  209. try {
  210. var pastedText = event.clipboardData
  211. ? event.clipboardData.getData("text/plain")
  212. : window.clipboardData.getData("Text"); // support IE
  213. var cleanedText = cleanPastedText(pastedText);
  214. // Pastes the cleaned up text.
  215. if (document.queryCommandSupported('insertText')) {
  216. document.execCommand('insertText', false, cleanedText);
  217. } else { // support IE
  218. document.execCommand('paste', false, cleanedText);
  219. }
  220. event.stopPropagation();
  221. event.preventDefault();
  222. return false;
  223. } catch (err) {
  224. // If anything goes wrong with browser compatibility,
  225. // pass the event through without modification.
  226. return true;
  227. }
  228. }
  229. // Removes junk that comes with pasting from text editors like Word.
  230. // taken from https://stackoverflow.com/questions/2875027/clean-microsoft-word-pasted-text-using-javascript
  231. function cleanPastedText(text) {
  232. return text.replace(/.*<!--.*-->/g, "");
  233. }
  234. // toggleVisible(id)
  235. // Toggles the visibility of a given element by given ID
  236. function toggleVisible(id) {
  237. var x = document.getElementById(id);
  238. if (x.style.display === "none") {
  239. x.style.display = "block";
  240. } else {
  241. x.style.display = "none";
  242. }
  243. }
  244. // classDisplayAll(className, value)
  245. // Assigns given display value to all elements with a given className
  246. function classDisplayAll(className, value) {
  247. var elements = document.getElementsByClassName(className);
  248. for (var i = 0; i < elements.length; i++) {
  249. elements[i].style.display = value;
  250. }
  251. }
  252. // toggleEditMode()
  253. // Toggles whether editable fields are editable or not
  254. // and removes editing tools.
  255. function toggleEditMode() {
  256. if (editMode === true) {
  257. disableEditMode();
  258. } else {
  259. enableEditMode();
  260. }
  261. }
  262. function disableEditMode() { // switch to preview mode
  263. editMode = false;
  264. document.getElementById("rulebox").classList.add('rulebox-preview');
  265. document.getElementById("rulebox").classList.remove('rulebox-edit');
  266. var editableFields = document.getElementsByClassName("editable");
  267. // de-editable-ize the editable fields
  268. for (var i = 0; i < editableFields.length; i++) {
  269. editableFields[i].contentEditable = "false";
  270. // Remove empty fields entirely
  271. var content = editableFields[i].innerHTML;
  272. content = stripHTML(content);
  273. if (content === "") {
  274. // editableFields[i].style.display = "none";
  275. }
  276. }
  277. // RuleBuilder sections
  278. if (builderEmpty()) {
  279. } else {
  280. document.getElementById("builder-field").innerHTML = displayBuilderHTML();
  281. }
  282. if (document.contains(document.getElementById("custom-field-container"))) {
  283. document.getElementById("custom-field-container").remove();
  284. }
  285. // Handle author link
  286. var authorName = document.getElementById("author-text").value;
  287. var authorURL = document.getElementById("author-url").value;
  288. if (authorName != "") {
  289. if (authorURL != "") { // both author and URL present
  290. document.getElementById("authorship-result").innerHTML = "<a href='" + authorURL +"'>" + authorName + "</a>";
  291. } else { // only authorName present
  292. document.getElementById("authorship-result").innerHTML = authorName;
  293. }
  294. document.getElementById("authorship").style.display = "";
  295. } else {
  296. document.getElementById("authorship").style.display = "none";
  297. }
  298. // Finally, change button name
  299. document.getElementById("editToggle").innerHTML = "Customize";
  300. }
  301. function enableEditMode() { // Switch to editMode
  302. editMode = true;
  303. document.getElementById("rulebox").classList.remove('rulebox-preview');
  304. document.getElementById("rulebox").classList.add('rulebox-edit');
  305. // RuleBuilder handling
  306. document.getElementById("builder-field").innerHTML = "";
  307. // make all editable fields visible
  308. var editableFields = document.getElementsByClassName("editable");
  309. for (var i = 0; i < editableFields.length; i++) {
  310. editableFields[i].contentEditable = "true";
  311. }
  312. // Change button name
  313. document.getElementById("editToggle").innerHTML = "Preview";
  314. }
  315. // toggleDisplayMode()
  316. // toggles full displayMode, the Rule-only display for a published Rule
  317. // first, initialize variable:
  318. var displayMode = false;
  319. function toggleDisplayMode() {
  320. if (displayMode == false) {
  321. editMode = true;
  322. toggleEditMode(); // turns off editMode
  323. classDisplayAll("site-nav","none");
  324. classDisplayAll("post-header","none");
  325. classDisplayAll("site-footer","none");
  326. document.getElementById("attribution").style.display = "block";
  327. document.getElementById("fork").style.display = "inline-block";
  328. document.getElementById("discuss-button").style.display = "inline-block";
  329. document.getElementById("publishRule").style.display = "none";
  330. document.getElementById("trash").style.display = "inline-block";
  331. // Turn on RuleWriter if there's content
  332. if ("" != document.getElementById("rulewriter").innerHTML) {
  333. document.getElementById("rulewriter-box").style.display = "inline-block";
  334. }
  335. // Finish
  336. displayMode = true;
  337. } else {
  338. toggleEditMode() // turns on editMode
  339. classDisplayAll("site-nav","block");
  340. classDisplayAll("post-header","block");
  341. classDisplayAll("site-footer","block");
  342. document.getElementById("attribution").style.display = "none";
  343. document.getElementById("fork").style.display = "none";
  344. document.getElementById("discuss-button").style.display = "none";
  345. document.getElementById("publishRule").style.display = "inline-block";
  346. document.getElementById("trash").style.display = "none";
  347. displayMode = false;
  348. }
  349. if (document.getElementById("lineage-list").innerHTML != "") {
  350. document.getElementById("lineage").style.display = "block";
  351. }
  352. }
  353. // textOutput()
  354. // Produces Markdown rendition of Rule from Export button
  355. function textOutput() {
  356. var filename = 'GOVERNANCE.md';
  357. // First, add title, whether there is one or not
  358. var content = '# '+ document.getElementById('communityname').innerHTML + '\n\n';
  359. content = stripHTML(content);
  360. // Next, add structure field
  361. var structure = document.getElementById('structure').innerHTML;
  362. if (structure != "") {
  363. content += stripHTML(structure) + '\n\n';
  364. }
  365. // Add Builder content
  366. if (!builderEmpty()) {
  367. content += displayBuilderMD() + "\n\n";
  368. }
  369. // Now, begin adding Writer elements
  370. var elements = document.getElementsByClassName('output');
  371. for (var i = 2; i < elements.length; i++) { // start after structure
  372. var thisBit = elements[i].innerHTML;
  373. thisBit = stripHTML(thisBit);
  374. if (thisBit != "") {
  375. if (elements[i].classList.contains("subhead")) {
  376. // Before printing subhead, make sure it's not empty
  377. var i2 = i + 1;
  378. while ((i2 < elements.length) &&
  379. (!(elements[i2].classList.contains("subhead")))) {
  380. if (elements[i2].innerHTML != "") {
  381. // in this case, it's not empty, so print and move on
  382. content += '## ';
  383. content += thisBit + '\n\n';
  384. break;
  385. } else { i2++; }
  386. } // won't print anything if a subhead has only empty children
  387. } else {
  388. // Non-subhead elements can just go ahead and print
  389. content += thisBit + '\n\n';
  390. }
  391. }
  392. }
  393. // Add authorship block
  394. var authorName = document.getElementById("author-text").value;
  395. var authorURL = document.getElementById("author-url").value;
  396. var authorshipBlock = "---\n\nCreated by ";
  397. if (authorName != "") {
  398. if (authorURL != "") { // both author and URL present
  399. authorshipBlock += ("[" + authorName + "](" + authorURL + ")");
  400. } else { // only authorName present
  401. authorshipBlock += authorName;
  402. }
  403. content += (authorshipBlock + "\n");
  404. }
  405. // Add attribution block
  406. content += document.getElementById('attributionMD').innerHTML;
  407. // Starting here, see https://stackoverflow.com/a/33542499
  408. var blob = new Blob([content], {type: 'text/plain'});
  409. if(window.navigator.msSaveOrOpenBlob) {
  410. window.navigator.msSaveBlob(blob, filename);
  411. }
  412. else{
  413. var elem = window.document.createElement('a');
  414. elem.href = window.URL.createObjectURL(blob);
  415. elem.download = filename;
  416. document.body.appendChild(elem);
  417. elem.click();
  418. document.body.removeChild(elem);
  419. URL.revokeObjectURL(); // This needs an arg but I can't figure out what
  420. }
  421. var myFile = new Blob([fileContent], {type: 'text/plain'});
  422. window.URL = window.URL || window.webkitURL; document.getElementById('download').setAttribute('href',window.URL.createObjectURL(myFile));
  423. document.getElementById('download').setAttribute('download', fileName);
  424. }
  425. // BEGIN Publish tools, via SteinHQ.com
  426. // publishRule()
  427. // Publishes existing fields to new page, /builder/?rule=[ruleID]
  428. // Opens new page in Display mode
  429. function publishRule() {
  430. // Confirm user knows what they're getting into
  431. var r = confirm("Publish to the public Library?");
  432. if (r == false) { return; }
  433. // Proceed with publication
  434. var now = new Date();
  435. // Numerical ID for published Rule
  436. var timeID = now.getTime();
  437. // Readable UTC timestamp
  438. var dateTime = now.getUTCFullYear()+'.'+(now.getUTCMonth()+1)+'.'+now.getUTCDate()
  439. +' '+now.getUTCHours()+":"+ now.getUTCMinutes()+":"+now.getUTCSeconds()
  440. + ' UTC';
  441. // Check if ruleID exists; while yes, replace and repeat
  442. var rule = [{
  443. ruleID: timeID,
  444. timestamp: dateTime,
  445. }];
  446. // begin adding data
  447. // first, RuleBuilder data
  448. document.getElementById("builder-field").innerHTML = ""; // so it doesn't publish
  449. if (!builderEmpty()) {
  450. rule[0]["modules"] = document.getElementById("module-input").innerHTML;
  451. }
  452. // next, RuleWriter data
  453. var fields = document.getElementsByClassName("editable");
  454. for (var i = 0; i < fields.length; i++) {
  455. var key = fields[i].id;
  456. var value = "";
  457. // including input fields
  458. if (fields[i].nodeName == "INPUT") { // for <input>
  459. value = fields[i].value.replace(/(<([^>]+)>)/ig,"");
  460. } else { // for other fields
  461. value = fields[i].innerHTML.replace(/(<([^>]+)>)/ig,"");
  462. }
  463. rule[0][key] = value;
  464. }
  465. // add to lineage (if it is a fork)
  466. if (rID) {
  467. rule[0]["lineage"] = document.getElementById("lineage-list").innerHTML;
  468. }
  469. // add to database
  470. const store = new SteinStore(
  471. "https://api.steinhq.com/v1/storages/5e8b937ab88d3d04ae0816a5"
  472. );
  473. store.append("library", rule).then(data => {
  474. window.open("/create/?r=" + timeID, "_self", false);
  475. });
  476. }
  477. // addLineage
  478. // Adds the current page to the lineage
  479. function addLineage() {
  480. var communityname = document.getElementById("communityname").innerHTML;
  481. var newLineage = " < " + '<a href="/create/?r=' + rID + '">'
  482. + communityname + '</a>';
  483. var oldLineage = document.getElementById("lineage-list").innerHTML;
  484. document.getElementById("lineage-list").innerHTML = newLineage + oldLineage;
  485. }
  486. // fork()
  487. // Forks the current Rule and updates the derivation lineage
  488. function fork() {
  489. document.getElementById("lineage").style.display = "block";
  490. addLineage();
  491. toggleDisplayMode();
  492. }
  493. // displayRule(ID)
  494. // Displays content based on ID
  495. function displayRule(ID) {
  496. const store = new SteinStore(
  497. "https://api.steinhq.com/v1/storages/5e8b937ab88d3d04ae0816a5"
  498. );
  499. (async () => {
  500. var sheets = ["library","templates"];
  501. // create empty set of rules (should only get one member)
  502. var ruleArray = [];
  503. // read values from all sheets
  504. for (var i = 0; i < sheets.length; i++) {
  505. await store.read(sheets[i], { search: { ruleID: ID } }).then(data => {
  506. // test if there's anything in data
  507. if (data.length > 0) {
  508. ruleArray = data;
  509. }
  510. });
  511. }
  512. // Only runs the rest if the array has something
  513. if (ruleArray.length > 0) {
  514. var rule = ruleArray[0];
  515. var fields = document.getElementsByClassName("editable");
  516. for (var i = 0; i < fields.length; i++) {
  517. var key = fields[i].id;
  518. var value = rule[key];
  519. if (typeof value === "undefined") {
  520. value = "";
  521. } else if (key.includes("-")) { // links
  522. document.getElementById(key).value = value;
  523. } else {
  524. document.getElementById(key).innerHTML = value;
  525. }
  526. }
  527. // Add Builder content
  528. document.getElementById("module-input").innerHTML = rule["modules"];
  529. // Add lineage
  530. var lineage = rule["lineage"];
  531. if (typeof lineage === "undefined") {
  532. document.getElementById("lineage-list").innerHTML = "";
  533. } else {
  534. document.getElementById("lineage-list").innerHTML = lineage;
  535. document.getElementById("lineage").display = "block";
  536. }
  537. // Publish timestamp to Rule
  538. document.getElementById('dateTime').innerHTML = rule['timestamp'];
  539. // Finish
  540. displayMode = false;
  541. toggleDisplayMode();
  542. document.title = rule['communityname'] + " / CommunityRule";
  543. }
  544. })();
  545. }
  546. // deleteRule()
  547. // A temporary placeholder that sends an email requesting rule deletion
  548. function deleteRule() {
  549. var urlParamz = new URLSearchParams(window.location.search);
  550. var rID = urlParamz.get('r');
  551. window.open("mailto:medlab@colorado.edu?subject=Delete Rule request ("
  552. + rID + ")&body=Please explain your rationale:\n");
  553. }
  554. // END Publish tools
  555. // FINALLY, Page loading
  556. // First, grab the current URL
  557. var urlParams = new URLSearchParams(window.location.search);
  558. // Determine if it is a published Rule
  559. if (urlParams.has('r')) {
  560. // If so, grab the Rule from database and enter displayMode
  561. var rID = urlParams.get('r');
  562. displayRule(rID);
  563. } else {
  564. // Otherwise, open in editMode as default
  565. var editMode = true;
  566. }
  567. // eqip editable fields to remove formatting from pasted content
  568. window.onload = function() {
  569. var editableElements = document.getElementsByClassName("editable");
  570. for (var i = 0; i < editableElements.length; i++ ) {
  571. editableElements[i].addEventListener("paste", handleEditablePaste);
  572. }
  573. }
  574. </script>