rule-scripts.html 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613
  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 <span class=\"module\">" + moduleName
  96. + "</span> 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. document.body.classList.add("display_rule");
  322. editMode = true;
  323. disableEditMode(); // turns off editMode
  324. classDisplayAll("site-nav","none");
  325. classDisplayAll("post-header","none");
  326. classDisplayAll("site-footer","none");
  327. document.getElementById("attribution").style.display = "block";
  328. document.getElementById("fork").style.display = "inline-block";
  329. document.getElementById("discuss-button").style.display = "inline-block";
  330. document.getElementById("publishRule").style.display = "none";
  331. document.getElementById("trash").style.display = "inline-block";
  332. // Turn on RuleWriter if there's content
  333. if ("" != document.getElementById("rulewriter").innerHTML) {
  334. document.getElementById("rulewriter-box").style.display = "inline-block";
  335. }
  336. // Finish
  337. displayMode = true;
  338. } else {
  339. document.body.classList.remove("display_rule");
  340. enableEditMode() // turns on editMode
  341. classDisplayAll("site-nav","block");
  342. classDisplayAll("post-header","block");
  343. classDisplayAll("site-footer","block");
  344. document.getElementById("attribution").style.display = "none";
  345. document.getElementById("fork").style.display = "none";
  346. document.getElementById("discuss-button").style.display = "none";
  347. document.getElementById("publishRule").style.display = "inline-block";
  348. document.getElementById("trash").style.display = "none";
  349. displayMode = false;
  350. }
  351. if (document.getElementById("lineage-list").innerHTML != "") {
  352. document.getElementById("lineage").style.display = "block";
  353. }
  354. }
  355. // textOutput()
  356. // Produces Markdown rendition of Rule from Export button
  357. function textOutput() {
  358. var filename = 'GOVERNANCE.md';
  359. // First, add title, whether there is one or not
  360. var content = '# '+ document.getElementById('communityname').innerHTML + '\n\n';
  361. content = stripHTML(content);
  362. // Next, add structure field
  363. var structure = document.getElementById('structure').innerHTML;
  364. if (structure != "") {
  365. content += stripHTML(structure) + '\n\n';
  366. }
  367. // Add Builder content
  368. if (!builderEmpty()) {
  369. content += displayBuilderMD() + "\n\n";
  370. }
  371. // Now, begin adding Writer elements
  372. var elements = document.getElementsByClassName('output');
  373. for (var i = 2; i < elements.length; i++) { // start after structure
  374. var thisBit = elements[i].innerHTML;
  375. thisBit = stripHTML(thisBit);
  376. if (thisBit != "") {
  377. if (elements[i].classList.contains("subhead")) {
  378. // Before printing subhead, make sure it's not empty
  379. var i2 = i + 1;
  380. while ((i2 < elements.length) &&
  381. (!(elements[i2].classList.contains("subhead")))) {
  382. if (elements[i2].innerHTML != "") {
  383. // in this case, it's not empty, so print and move on
  384. content += '## ';
  385. content += thisBit + '\n\n';
  386. break;
  387. } else { i2++; }
  388. } // won't print anything if a subhead has only empty children
  389. } else {
  390. // Non-subhead elements can just go ahead and print
  391. content += thisBit + '\n\n';
  392. }
  393. }
  394. }
  395. // Add authorship block
  396. var authorName = document.getElementById("author-text").value;
  397. var authorURL = document.getElementById("author-url").value;
  398. var authorshipBlock = "---\n\nCreated by ";
  399. if (authorName != "") {
  400. if (authorURL != "") { // both author and URL present
  401. authorshipBlock += ("[" + authorName + "](" + authorURL + ")");
  402. } else { // only authorName present
  403. authorshipBlock += authorName;
  404. }
  405. content += (authorshipBlock + "\n");
  406. }
  407. // Add attribution block
  408. content += document.getElementById('attributionMD').innerHTML;
  409. // Starting here, see https://stackoverflow.com/a/33542499
  410. var blob = new Blob([content], {type: 'text/plain'});
  411. if(window.navigator.msSaveOrOpenBlob) {
  412. window.navigator.msSaveBlob(blob, filename);
  413. }
  414. else{
  415. var elem = window.document.createElement('a');
  416. elem.href = window.URL.createObjectURL(blob);
  417. elem.download = filename;
  418. document.body.appendChild(elem);
  419. elem.click();
  420. document.body.removeChild(elem);
  421. URL.revokeObjectURL(); // This needs an arg but I can't figure out what
  422. }
  423. var myFile = new Blob([fileContent], {type: 'text/plain'});
  424. window.URL = window.URL || window.webkitURL; document.getElementById('download').setAttribute('href',window.URL.createObjectURL(myFile));
  425. document.getElementById('download').setAttribute('download', fileName);
  426. }
  427. // BEGIN Publish tools, via SteinHQ.com
  428. // publishRule()
  429. // Publishes existing fields to new page, /builder/?rule=[ruleID]
  430. // Opens new page in Display mode
  431. function publishRule() {
  432. // Confirm user knows what they're getting into
  433. var r = confirm("Publish to the public Library?");
  434. if (r == false) { return; }
  435. // Proceed with publication
  436. var now = new Date();
  437. // Numerical ID for published Rule
  438. var timeID = now.getTime();
  439. // Readable UTC timestamp
  440. var dateTime = now.getUTCFullYear()+'.'+(now.getUTCMonth()+1)+'.'+now.getUTCDate()
  441. +' '+now.getUTCHours()+":"+ now.getUTCMinutes()+":"+now.getUTCSeconds()
  442. + ' UTC';
  443. // Check if ruleID exists; while yes, replace and repeat
  444. var rule = [{
  445. ruleID: timeID,
  446. timestamp: dateTime,
  447. }];
  448. // begin adding data
  449. // first, RuleBuilder data
  450. document.getElementById("builder-field").innerHTML = ""; // so it doesn't publish
  451. if (!builderEmpty()) {
  452. rule[0]["modules"] = document.getElementById("module-input").innerHTML;
  453. }
  454. // next, RuleWriter data
  455. var fields = document.getElementsByClassName("editable");
  456. for (var i = 0; i < fields.length; i++) {
  457. var key = fields[i].id;
  458. var value = "";
  459. // including input fields
  460. if (fields[i].nodeName == "INPUT") { // for <input>
  461. value = fields[i].value.replace(/(<([^>]+)>)/ig,"");
  462. } else { // for other fields
  463. value = fields[i].innerHTML.replace(/(<([^>]+)>)/ig,"");
  464. }
  465. rule[0][key] = value;
  466. }
  467. // add to lineage (if it is a fork)
  468. if (rID) {
  469. rule[0]["lineage"] = document.getElementById("lineage-list").innerHTML;
  470. }
  471. // add to database
  472. const store = new SteinStore(
  473. "https://api.steinhq.com/v1/storages/5e8b937ab88d3d04ae0816a5"
  474. );
  475. store.append("library", rule).then(data => {
  476. window.open("/create/?r=" + timeID, "_self", false);
  477. });
  478. }
  479. // addLineage
  480. // Adds the current page to the lineage
  481. function addLineage() {
  482. var communityname = document.getElementById("communityname").innerHTML;
  483. var newLineage = " < " + '<a href="/create/?r=' + rID + '">'
  484. + communityname + '</a>';
  485. var oldLineage = document.getElementById("lineage-list").innerHTML;
  486. document.getElementById("lineage-list").innerHTML = newLineage + oldLineage;
  487. }
  488. // fork()
  489. // Forks the current Rule and updates the derivation lineage
  490. function fork() {
  491. document.getElementById("lineage").style.display = "block";
  492. addLineage();
  493. toggleDisplayMode();
  494. }
  495. // displayRule(ID)
  496. // Displays content based on ID
  497. function displayRule(ID) {
  498. const store = new SteinStore(
  499. "https://api.steinhq.com/v1/storages/5e8b937ab88d3d04ae0816a5"
  500. );
  501. (async () => {
  502. var sheets = ["library","templates"];
  503. // create empty set of rules (should only get one member)
  504. var ruleArray = [];
  505. // read values from all sheets
  506. for (var i = 0; i < sheets.length; i++) {
  507. await store.read(sheets[i], { search: { ruleID: ID } }).then(data => {
  508. // test if there's anything in data
  509. if (data.length > 0) {
  510. ruleArray = data;
  511. }
  512. });
  513. }
  514. // Only runs the rest if the array has something
  515. if (ruleArray.length > 0) {
  516. var rule = ruleArray[0];
  517. var fields = document.getElementsByClassName("editable");
  518. for (var i = 0; i < fields.length; i++) {
  519. var key = fields[i].id;
  520. var value = rule[key];
  521. if (typeof value === "undefined") {
  522. value = "";
  523. } else if (key.includes("-")) { // links
  524. document.getElementById(key).value = value;
  525. } else {
  526. document.getElementById(key).innerHTML = value;
  527. }
  528. }
  529. // Add Builder content
  530. document.getElementById("module-input").innerHTML = rule["modules"];
  531. // Add lineage
  532. var lineage = rule["lineage"];
  533. if (typeof lineage === "undefined") {
  534. document.getElementById("lineage-list").innerHTML = "";
  535. } else {
  536. document.getElementById("lineage-list").innerHTML = lineage;
  537. document.getElementById("lineage").display = "block";
  538. }
  539. // Publish timestamp to Rule
  540. document.getElementById('dateTime').innerHTML = rule['timestamp'];
  541. // Finish
  542. displayMode = false;
  543. toggleDisplayMode();
  544. document.title = rule['communityname'] + " / CommunityRule";
  545. }
  546. })();
  547. }
  548. // deleteRule()
  549. // A temporary placeholder that sends an email requesting rule deletion
  550. function deleteRule() {
  551. var urlParamz = new URLSearchParams(window.location.search);
  552. var rID = urlParamz.get('r');
  553. window.open("mailto:medlab@colorado.edu?subject=Delete Rule request ("
  554. + rID + ")&body=Please explain your rationale:\n");
  555. }
  556. // END Publish tools
  557. var rID;
  558. window.onload = function() {
  559. // FINALLY, Page loading
  560. // First, grab the current URL
  561. var urlParams = new URLSearchParams(window.location.search);
  562. // Determine if it is a published Rule
  563. if (urlParams.has('r')) {
  564. // If so, grab the Rule from database and enter displayMode
  565. rID = urlParams.get('r');
  566. document.body.classList.add("display_rule");
  567. displayRule(rID);
  568. } else {
  569. // Otherwise, open in editMode as default
  570. enableEditMode();
  571. }
  572. // eqip editable fields to remove formatting from pasted content
  573. var editableElements = document.getElementsByClassName("editable");
  574. for (var i = 0; i < editableElements.length; i++ ) {
  575. editableElements[i].addEventListener("paste", handleEditablePaste);
  576. }
  577. }
  578. </script>