vue.rules.js 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212
  1. /**
  2. * The Vue application instance.
  3. * https://vuejs.org/guide/essentials/application.html
  4. * This is the root component
  5. */
  6. const app = Vue.createApp({
  7. /**
  8. * to prevent conflict with jekyll we change the
  9. * delimiters from {{ }} to [[ ]]
  10. */
  11. delimiters: ['[[', ']]'],
  12. /**
  13. * The data object of the root component.
  14. * These variables are available in the html template.
  15. */
  16. data() {
  17. return {
  18. // state of the editor; used for undo
  19. state: {
  20. // The rule object
  21. rule: global.rule || {
  22. ruleID: "",
  23. timestamp: "",
  24. icon: "",
  25. name: "",
  26. lineage: "",
  27. summary: "",
  28. config: {},
  29. creator: {
  30. name: "",
  31. url: "",
  32. },
  33. modules: []
  34. },
  35. // the data of the current module in the editor
  36. editor: {
  37. source: null,
  38. previousState: null,
  39. module: null,
  40. },
  41. },
  42. legacy: false,
  43. rID: false,
  44. loading: false,
  45. publishing: false,
  46. view: (global.rule) ? true : false,
  47. preview: (global.rule) ? true : false,
  48. template: (global.rule) ? true : false,
  49. steinAPI: 'https://api.steinhq.com/v1/storages/5e8b937ab88d3d04ae0816a5',
  50. // keep an array of past states for undo/redo
  51. currentStateIndex: -1,
  52. history: [],
  53. // the module that is currently being dragged
  54. holding: false,
  55. // object of icons for easy access
  56. icons: {
  57. plus: '/assets/tabler_icons/circle-plus.svg',
  58. info: '/assets/tabler_icons/info-circle.svg',
  59. chevron: '/assets/tabler_icons/chevrons-down.svg',
  60. blank: '/assets/tabler_icons/circle-dotted.svg',
  61. view: '/assets/tabler_icons/eye.svg',
  62. edit: '/assets/tabler_icons/tool.svg',
  63. plus: '/assets/tabler_icons/plus.svg',
  64. minus: '/assets/tabler_icons/minus.svg',
  65. publish: '/assets/tabler_icons/cloud-upload.svg',
  66. download: '/assets/tabler_icons/download.svg',
  67. export: '/assets/tabler_icons/file-download.svg',
  68. upload: '/assets/tabler_icons/file-upload.svg',
  69. fork: '/assets/tabler_icons/git-fork.svg',
  70. },
  71. // icons available in the editor
  72. moduleIcons: {
  73. culture: '/assets/tabler_icons/palette.svg',
  74. decision: '/assets/tabler_icons/thumb-up.svg',
  75. process: '/assets/tabler_icons/rotate.svg',
  76. structure: '/assets/tabler_icons/building.svg',
  77. relationship: '/assets/tabler_icons/heart.svg',
  78. economic: '/assets/tabler_icons/coin.svg',
  79. legal: '/assets/tabler_icons/license.svg',
  80. map: '/assets/tabler_icons/map.svg',
  81. communications: '/assets/tabler_icons/microphone.svg',
  82. },
  83. // icons available for rules
  84. ruleIcons: {
  85. atom: '/assets/tabler_icons/atom.svg',
  86. bandage: '/assets/tabler_icons/bandage.svg',
  87. book: '/assets/tabler_icons/book.svg',
  88. box: '/assets/tabler_icons/box.svg',
  89. church: '/assets/tabler_icons/building-church.svg',
  90. store: '/assets/tabler_icons/building-store.svg',
  91. brush: '/assets/tabler_icons/brush.svg',
  92. car: '/assets/tabler_icons/car.svg',
  93. clock: '/assets/tabler_icons/clock.svg',
  94. cloud: '/assets/tabler_icons/cloud.svg',
  95. compass: '/assets/tabler_icons/compass.svg',
  96. game: '/assets/tabler_icons/device-gamepad.svg',
  97. flask: '/assets/tabler_icons/flask.svg',
  98. location: '/assets/tabler_icons/location.svg',
  99. moon: '/assets/tabler_icons/moon.svg',
  100. settings: '/assets/tabler_icons/settings.svg',
  101. shield: '/assets/tabler_icons/shield.svg',
  102. star: '/assets/tabler_icons/star.svg',
  103. tool: '/assets/tabler_icons/tool.svg',
  104. world: '/assets/tabler_icons/world.svg',
  105. },
  106. // the template for modules
  107. moduleTemplate: {
  108. moduleID: "",
  109. name: "",
  110. icon: "",
  111. summary: "",
  112. config: {},
  113. type: "",
  114. modules: []
  115. },
  116. // tracks the current module library tab that is open
  117. moduleLibrary: 'culture',
  118. moduleTypes: {
  119. // custom: {
  120. // question: 'Modules that you\'ve created.',
  121. // icon: '/assets/tabler_icons/circle-plus.svg',
  122. // open: true
  123. // },
  124. culture: {
  125. question: 'What are the core missions, values, and norms?',
  126. icon: '/assets/tabler_icons/palette.svg',
  127. open: true
  128. },
  129. decision: {
  130. question: 'Who can make decisions and how?',
  131. icon: '/assets/tabler_icons/thumb-up.svg',
  132. open: false
  133. },
  134. process: {
  135. question: 'How are policies implemented, and how do they evolve?',
  136. icon: '/assets/tabler_icons/rotate.svg ',
  137. open: false
  138. },
  139. structure: {
  140. question: 'What kinds of roles and internal entities are there?',
  141. icon: '/assets/tabler_icons/building.svg',
  142. open: false
  143. }
  144. },
  145. // array of modules that have been created by the user
  146. // TODO: implement custom modules
  147. customModules: [],
  148. // library of existing modules
  149. modules: global.modules,
  150. exports: {
  151. markdown: null,
  152. json: null,
  153. }
  154. }
  155. },
  156. // on load
  157. mounted() {
  158. // put initial state into this.history
  159. this.initializeUndoFunctionality()
  160. },
  161. /**
  162. * Vue provide passes data to other components.
  163. * https://vuejs.org/guide/components/provide-inject.html#provide-inject
  164. */
  165. provide() {
  166. return {
  167. editor: this.state.editor,
  168. icons: this.icons,
  169. }
  170. },
  171. created() {
  172. this.addToEditor(this.newModule());
  173. var urlParams = new URLSearchParams(window.location.search);
  174. if (urlParams.has('r')) {
  175. this.rID = urlParams.get('r');
  176. this.preview = true;
  177. this.view = true;
  178. this.fetchRule(this.rID);
  179. }
  180. },
  181. computed: {
  182. /**
  183. * Exports the current rule into a normalized format
  184. * Cleans up all submodules so they are ready for export
  185. * @returns {Object} a Rule object
  186. */
  187. ruleExport() {
  188. //TODO: test if icon is an absolute url and only add the global if it is not
  189. /**
  190. * Takes a module and recursively cleans it and all submodules up
  191. * @param {Object} module a module object
  192. * @returns
  193. */
  194. function cleanModules(module) {
  195. const newModule = {
  196. moduleID: module.moduleID,
  197. name: module.name,
  198. icon: (module.icon && !module.icon.includes('http')) ? global.url + module.icon : module.icon,
  199. summary: module.summary,
  200. config: module.config,
  201. type: module.type,
  202. modules: (module.modules) ? module.modules.map(cleanModules) : [],
  203. }
  204. return newModule;
  205. }
  206. return {
  207. ruleID: (this.state.rule.ruleID) ? this.state.rule.ruleID : this.slugify(this.state.rule.name),
  208. timestamp: this.timesString(),
  209. icon: (this.state.rule.icon && !this.state.rule.icon.includes('http')) ? global.url + this.state.rule.icon : this.state.rule.icon,
  210. name: this.state.rule.name,
  211. lineage: this.state.rule.lineage,
  212. summary: this.state.rule.summary,
  213. config: this.state.rule.config,
  214. creator: {
  215. name: this.state.rule.creator.name,
  216. url: this.state.rule.creator.url,
  217. },
  218. modules: this.state.rule.modules.map(cleanModules)
  219. }
  220. },
  221. /**
  222. * @returns {String} the current rule as a JSON string
  223. */
  224. json() {
  225. return JSON.stringify(this.ruleExport, null, 2);
  226. },
  227. /**
  228. * Creates an array of all moduleIDs in use
  229. * @returns {Array} an array of all module's (in the library and custom modules) moduleID
  230. */
  231. listModuleIds() {
  232. const modules = [...this.state.rule.modules, ...this.customModules];
  233. return modules.map(module => module.moduleID)
  234. },
  235. /**
  236. * @returns {Object} the current module in the editor
  237. */
  238. moduleInEditor() {
  239. return this.moduleEditor[0]
  240. },
  241. /**
  242. * Tests if the current module in the editor has been modified
  243. * against the editor.previousState object
  244. * @returns {Boolean} true if the module in the editor has been modified
  245. */
  246. //TODO: find a more accurate solution than just turning the object into a string
  247. editorHasEdits() {
  248. return this.state.editor.module && Object.entries(this.state.editor.module).toString() !== Object.entries(this.state.editor.previousState).toString();
  249. },
  250. /**
  251. * required to update editor on state updates
  252. */
  253. computedState() {
  254. // state of the editor; used for undo
  255. return this.state;
  256. }
  257. },
  258. methods: {
  259. // module methods ===========================================================
  260. /**
  261. * @returns {Object} a new module object from the moduleTemplate
  262. */
  263. newModule() {
  264. return JSON.parse(JSON.stringify(this.moduleTemplate));
  265. },
  266. /**
  267. * spreads a source module into a new module from the moduleTemplate
  268. * @param {Object} sourceModule the module to copy
  269. * @param {Boolean} includeSubmodules whether to copy submodules or not
  270. * @returns
  271. */
  272. cloneModule(sourceModule,includeSubmodules) {
  273. let output = {
  274. ...this.moduleTemplate,
  275. ...sourceModule,
  276. //TODO: implement lineage pattern, same as the rule does
  277. source: sourceModule, // keep track of where this module came from
  278. };
  279. if (!includeSubmodules) output.modules = [];
  280. // clear configs
  281. document.getElementById("newConfigKey").value =
  282. "Configuration";
  283. document.getElementById("newConfigValue").value =
  284. "Value";
  285. // delete unnecessary properties
  286. delete output.content;
  287. delete output.readonly;
  288. // TODO: give module a unique id
  289. // if (output.moduleID) output.moduleID = this.getUniqueId(output.moduleID);
  290. return output;
  291. },
  292. /**
  293. * Handles the click event to copy a module
  294. * @param {Event} ev the click event
  295. */
  296. handleClickCopyModule(ev) {
  297. const clickTarget = this.getClosestModule(ev.target);
  298. if (!clickTarget) return;
  299. this.copyModule(clickTarget.module);
  300. },
  301. /**
  302. * Handles the click event to edit a module
  303. * @param {Event} ev the click event
  304. */
  305. handleClickEditModule(ev) {
  306. const clickTarget = this.getClosestModule(ev.target);
  307. if (!clickTarget) return;
  308. this.editModule(clickTarget.module);
  309. },
  310. /**
  311. * Copies a module to the editor
  312. * @param {Object} module the module to copy
  313. */
  314. copyModule(module) {
  315. this.copyToEditor(module);
  316. },
  317. /**
  318. * moves a module to the editor
  319. * @param {Object} module the module to edit
  320. */
  321. editModule(module) {
  322. this.addToEditor(module);
  323. },
  324. /**
  325. * add a module to another module (or to the rule by default) as a submodule
  326. * @param {Object} module to add
  327. * @param {Object} target a module or the rule Object where the module should be added as a submodule
  328. */
  329. addModule(module,target = this.state.rule) {
  330. target.modules.push(module);
  331. },
  332. /**
  333. * remove a module from another module (or from the rule)
  334. * recursively moves through all submodules in the target
  335. * removes ONLY the first instance of the module
  336. * @param {Object} module the module to remove from target
  337. * @param {Object} target the module or rule to remove the module from (defaults to rule)
  338. */
  339. removeModule(module, target = this.state.rule) {
  340. if (! this.moduleContains(module, target)) return; // if the module is not in the target, do nothing
  341. //
  342. target.modules.forEach((m, idx) => {
  343. if (m === module) {
  344. target.modules.splice(idx, 1);
  345. return;
  346. } else {
  347. this.removeModule(module, m);
  348. }
  349. });
  350. },
  351. /**
  352. * Deletes custom module from the customModules array and clears the editor
  353. * @param {Object} module the module to be deleted
  354. */
  355. deleteModule(module) {
  356. this.removeCustomModule(module);
  357. // TODO: only clear the editor if the module is present in the editor
  358. this.clearEditor();
  359. },
  360. /**
  361. * Handles the start drag event for a module
  362. * @param {Event} ev the drag event
  363. */
  364. startDragModule(ev) {
  365. const targetModule = this.getClosestModule(ev.target);
  366. if (!targetModule) return;
  367. const module = targetModule.module;
  368. ev.dataTransfer.setDragImage(targetModule, ev.offsetX, ev.offsetY);
  369. this.holding = {module};
  370. },
  371. /**
  372. * Handles the start drag event for a module
  373. * when the module is being rearranged within the rule
  374. * @param {Event} ev the drag event
  375. */
  376. rearrangeModule(ev) {
  377. const targetModule = this.getClosestModule(ev.target);
  378. if (!targetModule) return;
  379. const source = this.getClosestModule(targetModule.parentNode).module;
  380. const module = targetModule.module;
  381. ev.dataTransfer.setDragImage(targetModule, ev.offsetX, ev.offsetY);
  382. this.holding = {
  383. module,
  384. source,
  385. };
  386. },
  387. /**
  388. * Handles the dragend event for a module
  389. */
  390. endDragModule() {
  391. this.holding = false;
  392. },
  393. /**
  394. * Recursively searches modules and their submodules for a module
  395. * @param {Object} needle the module to search for
  396. * @param {Object} haystack the module to search in (defaults to rule)
  397. * @returns {Boolean} true if the module is in the haystack
  398. */
  399. // TODO: return the module location in the haystack (Maybe?)
  400. moduleContains(needle, haystack = this.state.rule) {
  401. if (! haystack.modules ) return false; // does the haystack even have modules?
  402. if (haystack.modules.includes(needle)) return true; // is the needle in the haystack?
  403. return haystack.modules.some(submodule => this.moduleContains(needle, submodule)); // is the needle in any of the submodules?
  404. },
  405. // rule methods ===========================================================
  406. /**
  407. * Handles the drop event for a module
  408. * adds the module to the closest submodule or the rule depending on what it is dropped onto
  409. * then adds the module to the editor
  410. * @param {Event} ev the drop event
  411. */
  412. dropOnRule(ev) {
  413. //TODO browser drag objects that hover over drop zone are showing a 'add' icon
  414. const landingNode = this.getClosestModule(ev.target);
  415. if (!this.holding.module || !landingNode) return; // if there is no module to drop or no landing node, do nothing
  416. const landingModule = landingNode.module; // module is set with the v-bind prop binding
  417. const holdingModule = this.holding.module;
  418. if (holdingModule === landingModule) return; // if the module is the same, do nothing
  419. // if the module being dropped is readyonly clone it, otherwise use the original
  420. const readonly = holdingModule.readonly;
  421. const module = (readonly) ? this.cloneModule(holdingModule) : holdingModule;
  422. if (this.holding.source) {
  423. // if the module has a source, remove it from that source
  424. this.removeModule(holdingModule, this.holding.source);
  425. }
  426. this.addModule(module, landingModule);
  427. this.editModule(module);
  428. this.endDragModule();
  429. },
  430. fetchRule(id) {
  431. this.loading = true;
  432. // handle legacy links
  433. // TODO: handle this at a global level
  434. let redirect = {
  435. 'benevolent_dictator': 'benevolent-dictator',
  436. circles: 'circles',
  437. consensus: 'consensus',
  438. 'do-ocracy': 'do-ocracy',
  439. 'elected_board': 'elected-board',
  440. jury: 'jury',
  441. petition: 'petition',
  442. 'self-appointed_board': 'self-appointed-board',
  443. }
  444. // if the rule is a legacy link, redirect
  445. if (redirect[id]) {
  446. location.href = `/templates/${redirect[id]}`;
  447. return;
  448. }
  449. const store = new SteinStore(
  450. this.steinAPI
  451. );
  452. (async () => {
  453. var rule = [];
  454. // read values from all sheets
  455. await store.read('rules', { search: { ruleID: id } }).then(data => {
  456. // test if there's anything in data
  457. if (data.length > 0) {
  458. rule = data[0];
  459. }
  460. console.log(rule);
  461. });
  462. // no rule found, exit
  463. // TODO: inform the user that the rule was not found
  464. if (!rule) {
  465. this.loading = false;
  466. return;
  467. }
  468. // if this is a legacy (pre-v3) set it as such
  469. if (rule.version < 3) {
  470. this.loading = false;
  471. this.legacy = true;
  472. this.state.rule = rule;
  473. Vue.nextTick(() => {
  474. if (rule.version == 2) displayBuilderHTML();
  475. });
  476. return;
  477. }
  478. this.state.rule = {
  479. ruleID: rule.ruleID,
  480. timestamp: rule.timestamp,
  481. icon: rule.icon,
  482. name: rule.name,
  483. lineage: rule.lineage,
  484. summary: rule.summary,
  485. config: rule.config,
  486. creator: {
  487. name: rule.creatorName,
  488. url: rule.creatorUrl,
  489. },
  490. modules: (rule.modules) ? JSON.parse(rule.modules) : []
  491. }
  492. /** Add name to <title> for v3+ rules */
  493. document.title = rule.name + " / CommunityRule"
  494. this.loading = false;
  495. })();
  496. },
  497. // editor methods =========================================================
  498. /**
  499. * Adds a module to the editor
  500. * @param {Object} module the module to add to the editor
  501. */
  502. addToEditor(module) {
  503. this.preventEditorLoss();
  504. this.setEditorSource(module);
  505. this.setEditorPreviousState(module);
  506. this.state.editor.module = module;
  507. },
  508. /**
  509. * Copies a module to the editor
  510. * @param {Object} module the module to copy to the editor
  511. */
  512. copyToEditor(module) {
  513. const moduleCopy = this.cloneModule(module);
  514. this.preventEditorLoss();
  515. this.setEditorSource(module);
  516. this.setEditorPreviousState(moduleCopy);
  517. this.state.editor.module = moduleCopy;
  518. },
  519. /**
  520. * Takes a module and clones it into the editor.previousState
  521. * @param {Object} module the module to add to the previous state
  522. */
  523. setEditorPreviousState(module) {
  524. this.state.editor.previousState = { ...module };
  525. },
  526. /**
  527. * Sets the editor.source to the module
  528. * @param {Object} module the module to set the editor source to
  529. */
  530. setEditorSource(module) {
  531. this.state.editor.source = module;
  532. },
  533. /**
  534. * Checks if the editor has edits and that the current module in the editor is not present in the rule
  535. * If the module in the editor would be lost, confirm with the user
  536. * then adds the module to the customModules array
  537. */
  538. preventEditorLoss() {
  539. // if the editor has changes and the module isn't in the rule, save it to the custom modules
  540. if (this.state.editorHasEdits && !this.moduleContains(this.state.editor.module)) {
  541. this.confirm('You have unsaved changes. Are you sure you want to discard them?')
  542. this.addCustomModule(this.state.editor.module);
  543. }
  544. },
  545. /**
  546. * Handles the click event for adding a module from the editor to the rule
  547. */
  548. clickAddModule() {
  549. const module = this.state.editor.module;
  550. this.addModule(module);
  551. this.addToEditor(module);
  552. },
  553. /**
  554. * Handles the click event for removing a module in the editor from the rule
  555. */
  556. clickRemoveModule() {
  557. const module = this.state.editor.module;
  558. this.removeModule(module);
  559. },
  560. /**
  561. * Clears the editor
  562. */
  563. clearEditor() {
  564. this.preventEditorLoss();
  565. this.state.editor.module = null;
  566. this.state.editor.previousState = null;
  567. },
  568. /**
  569. * Saves the module in the editor to customModules array
  570. */
  571. saveEditor() {
  572. this.addCustomModule(this.state.editor.module);
  573. this.setEditorPreviousState(this.state.editor.module);
  574. },
  575. // config methods =========================================================
  576. /**
  577. * Add custom config entry to the module
  578. */
  579. addConfig() {
  580. const k = document.getElementById("newConfigKey").value;
  581. const v = document.getElementById("newConfigValue").value;
  582. this.state.editor.module.config[k] = v;
  583. this.resetNewConfigInputs();
  584. },
  585. /**
  586. * Removes the config entry from the module
  587. */
  588. removeConfig(key) {
  589. delete this.state.editor.module.config[key];
  590. },
  591. // custom module methods ==================================================
  592. /**
  593. * Adds a module to the customModules array
  594. * @param {Object} module the module to add to the customModules array
  595. */
  596. addCustomModule(module) {
  597. // if module is not in customModules, add it
  598. if (!this.customModules.includes(module)) {
  599. this.customModules.unshift(module);
  600. }
  601. },
  602. /**
  603. * Creates a new module, sets a default name, and adds it to the editor
  604. */
  605. newCustomModule() {
  606. const module = this.newModule();
  607. module.name = 'New Module';
  608. module.config = {};
  609. this.resetNewConfigInputs();
  610. this.addToEditor(module);
  611. },
  612. resetNewConfigInputs() {
  613. document.getElementById("newConfigKey").value = "Configuration";
  614. document.getElementById("newConfigValue").value = "Value";
  615. },
  616. /**
  617. * Removes a module from the customModules array
  618. * @param {Object} module the module to remove from the customModules array
  619. */
  620. removeCustomModule(module) {
  621. this.confirm("are you sure you want to remove this custom module?");
  622. const index = this.customModules.indexOf(module);
  623. this.customModules.splice(index, 1);
  624. },
  625. /**
  626. * Handles confirmation messages for users
  627. * @param {String} msg the message to display in the confirm dialog
  628. */
  629. // TODO: add a confirm dialog and return boolean based on user input
  630. confirm(msg) {
  631. console.log(msg);
  632. },
  633. // export and download methods =============================================
  634. /**
  635. * Handles click event for publishing the rule
  636. */
  637. handleClickPublish() {
  638. // Confirm user knows what they're getting into
  639. if (!confirm("Publish to the public Library?")) return;
  640. if ( !this.state.rule.name ) {
  641. alert("Please enter a name for this rule.");
  642. return;
  643. }
  644. if ( this.state.rule.modules.length < 1 ) {
  645. alert("Please add at least one module to this rule.");
  646. return;
  647. }
  648. this.publishing = true;
  649. const rule = this.ruleExport;
  650. const ruleID = new Date().getTime(); // TODO: allow for custom named IDs, check for uniqueness
  651. // add to database
  652. const store = new SteinStore(
  653. this.steinAPI
  654. );
  655. store.append('rules', [{
  656. ruleID: ruleID,
  657. timestamp: rule.timestamp,
  658. icon: rule.icon,
  659. name: rule.name,
  660. lineage: rule.lineage,
  661. summary: rule.summary,
  662. config: this.jsonify(rule.config),
  663. modules: this.jsonify(rule.modules),
  664. creatorName: rule.creator.name,
  665. creatorUrl: rule.creator.url,
  666. version: 3
  667. }]).then(data => {
  668. this.publishing = false;
  669. window.open("/create/?r=" + ruleID, "_self", false);
  670. });
  671. },
  672. /**
  673. * Handles the click event for downloading the rule as a Markdown file
  674. * Creates a YAML string of the rule
  675. * Then adds it to the bottom of a markdown file
  676. * created from the #rule-export html element
  677. */
  678. handleClickDownload() {
  679. const yaml = jsyaml.dump(this.ruleExport);
  680. const turndown = new TurndownService();
  681. const html = document.getElementById('rule-export');
  682. if (!html) return;
  683. const markdown = turndown.turndown(html);
  684. const output = markdown + '\n\n----\n```yaml\n' + yaml + '\n```';
  685. this.saveFile(`${this.ruleExport.ruleID}.md`, output, 'text/markdown');
  686. },
  687. /**
  688. * IE10+ Firefox, and Chrome method for saving a file
  689. * https://stackoverflow.com/a/33542499
  690. * @param {String} filename name of the file to save
  691. * @param {String} data data to save into a file
  692. * @param {String} type MIME type of the file
  693. */
  694. saveFile(filename, data, type) {
  695. const blob = new Blob([data], { type: type });
  696. if (window.navigator.msSaveOrOpenBlob) {
  697. window.navigator.msSaveBlob(blob, filename);
  698. }
  699. else {
  700. const elem = window.document.createElement('a');
  701. elem.href = window.URL.createObjectURL(blob);
  702. elem.download = filename;
  703. document.body.appendChild(elem);
  704. elem.click();
  705. document.body.removeChild(elem);
  706. }
  707. },
  708. /**
  709. * Handles the click event for importing a rule from a JSON file
  710. */
  711. handleClickImport() {
  712. },
  713. handleStateUpdate()
  714. {
  715. const MAX_UNDOS_ALLOWED = 15;
  716. this.currentStateIndex += 1;
  717. this.history[this.currentStateIndex] = JSON.parse(JSON.stringify(this.state));
  718. // if we have >1 states in the redo stack (i.e. history past our current index)
  719. if (this.currentStateIndex != this.history.length - 1)
  720. {
  721. // truncate history, so we can't redo out of order.
  722. this.history.length = this.currentStateIndex + 1; // truncates forked redo states
  723. }
  724. while (this.history.length > MAX_UNDOS_ALLOWED)
  725. {
  726. this.history.shift();
  727. this.currentStateIndex -= 1;
  728. }
  729. },
  730. canUndo()
  731. {
  732. return this.currentStateIndex > 0;
  733. },
  734. /**
  735. * handles undo by shifting the state back one index in the history
  736. */
  737. handleUndo()
  738. {
  739. this.appComponent.focus();
  740. if (this.canUndo())
  741. {
  742. this.currentStateIndex -= 1;
  743. this.state = this.history[this.currentStateIndex];
  744. }
  745. else
  746. {
  747. console.warn('No more actions to undo!')
  748. }
  749. },
  750. canRedo()
  751. {
  752. return this.currentStateIndex < this.history.length - 1;
  753. },
  754. /**
  755. * handles redo by shifting the currentStateIndex forward
  756. */
  757. handleRedo()
  758. {
  759. this.appComponent.focus();
  760. if (this.canRedo())
  761. {
  762. this.currentStateIndex += 1;
  763. this.state = this.history[this.currentStateIndex]
  764. }
  765. else
  766. {
  767. console.warn('No more actions to redo!')
  768. }
  769. },
  770. /**
  771. * initializes undo capabilities
  772. */
  773. initializeUndoFunctionality()
  774. {
  775. this.appComponent = document.getElementById('app'); // todo this doesn't work yet
  776. this.handleStateUpdate();
  777. const component = this;
  778. document.addEventListener('keydown', function (event)
  779. {
  780. console.log('keypress event:')
  781. console.log(event)
  782. console.log('ctrl?', event.ctrlKey)
  783. if (component.getModifierKeyByOs(event) && event.key === 'z')
  784. {
  785. event.preventDefault();
  786. if (event.shiftKey)
  787. {
  788. component.handleRedo();
  789. }
  790. else
  791. {
  792. component.handleUndo();
  793. }
  794. }
  795. });
  796. },
  797. /**
  798. * the modifier key is different for mac vs windows.
  799. * this should (but does not yet) return
  800. * event.ctrlKey if os == windows
  801. * event.metaKey if os == mac
  802. * @param {*} event
  803. * @returns bool: true if the os-specific modifier key is depressed
  804. */
  805. getModifierKeyByOs(event) {
  806. // TODO: this allows ctrl+z on a mac to undo, which isn't normal.
  807. // it might also allow windows key + z to undo on windows.
  808. return (event.metaKey || event.ctrlKey)
  809. },
  810. // utility methods ===========================================================
  811. /**
  812. * Takes an html element and finds the closest node (inclusive) that has a data-module-id attribute
  813. * @param {Node} el the html element to check
  814. * @returns {Node} the closest node with a data-module-id attribute
  815. */
  816. getClosestModule(el) {
  817. const parent = el.closest('[data-module-id]');
  818. if (!parent) return false;
  819. if (!parent.module) return false;
  820. return parent;
  821. },
  822. /**
  823. * Handles the click event for activating the rule preview
  824. */
  825. clickPreview() {
  826. if(this.template) this.state.rule.icon = ''; // TODO: find a less hacky way to reset template icons
  827. this.view = false;
  828. this.preview = !this.preview;
  829. },
  830. /**
  831. * Filters module library based on the search term
  832. * @param {String} type the name of the type to filter
  833. * @returns {Array} the filtered module library
  834. */
  835. getModulesByType(type) {
  836. return this.modules.filter(module => module.type === type)
  837. },
  838. /**
  839. * Slugifies a string
  840. * https://gist.github.com/codeguy/6684588 (one of the comments)
  841. * @param {String} string the string to slugify
  842. * @returns
  843. */
  844. slugify(string) {
  845. const separator = '-';
  846. return string
  847. .toString()
  848. .normalize('NFD') // split an accented letter in the base letter and the accent
  849. .replace(/[\u0300-\u036f]/g, '') // remove all previously split accents
  850. .toLowerCase()
  851. .replace(/[^a-z0-9 -]/g, '') // remove all chars not letters, numbers and spaces (to be replaced)
  852. .trim()
  853. .replace(/\s+/g, separator);
  854. },
  855. /**
  856. * Creates a human readable timestamp string
  857. * @param {String} date optional date to format
  858. * @returns {String} human readable date '2022.4.12 14:44:56 UTC'
  859. */
  860. timesString(date) {
  861. let now = new Date(date);
  862. if (isNaN(now)) now = new Date();
  863. return now.getUTCFullYear() + '.' + (now.getUTCMonth() + 1) + '.' + now.getUTCDate()
  864. + ' ' + now.getUTCHours() + ":" + now.getUTCMinutes() + ":" + now.getUTCSeconds()
  865. + ' UTC';
  866. },
  867. /**
  868. * stringify an Object
  869. * @param {Object} obj
  870. * @returns JSON string
  871. */
  872. jsonify(obj) {
  873. return JSON.stringify(obj, null, 2);
  874. },
  875. /**
  876. * Takes a moduleID and checks if that moduleID is in use
  877. * if so, returns the moduleID with a number appended to it
  878. * @param {String} test the moduleID to test
  879. * @returns {String} the moduleID, or if in use, with a number appended to it
  880. */
  881. getUniqueId(test) {
  882. let id = test;
  883. let i = 0;
  884. while (this.listModuleIds.includes(id)) {
  885. i++;
  886. id = `${test}-${i}`;
  887. }
  888. return id
  889. },
  890. },
  891. });
  892. /**
  893. * The Module Vue Component
  894. */
  895. app.component('module', {
  896. delimiters: ['[[', ']]'],
  897. inject: ['editor'],
  898. props: {
  899. module: {
  900. type: Object,
  901. required: true,
  902. },
  903. inEditor: {
  904. type: Boolean,
  905. default: false,
  906. },
  907. hideSubmodules: {
  908. type: Boolean,
  909. default: false,
  910. }
  911. },
  912. data() {
  913. return {
  914. defaultIcon: '/assets/tabler_icons/circle-dotted.svg',
  915. mouseOver: false,
  916. dragOver: false
  917. }
  918. },
  919. computed: {
  920. icon() {
  921. return this.module.icon ? this.module.icon : this.defaultIcon;
  922. },
  923. moduleClass() {
  924. return {
  925. 'in-editor': this.editor.source == this.module,
  926. 'mouse-over': this.mouseOver,
  927. // TODO: when dragging over the icon the drag-over class disappears
  928. 'drag-over': this.dragOver
  929. }
  930. }
  931. },
  932. template: `
  933. <div
  934. class="module"
  935. :class="moduleClass"
  936. .module="module"
  937. @mouseover.stop="this.mouseOver = true"
  938. @mouseout="this.mouseOver = false"
  939. @dragenter.self="this.dragOver = true"
  940. @dragleave.self="this.dragOver = false"
  941. @drop.self="this.dragOver = false"
  942. :data-module-id="module.moduleID"
  943. >
  944. <div class="module__icon-wrapper">
  945. <span class="module__grain"><img src="/assets/tabler_icons/grain.svg"></span>
  946. <span class="module__icon"><img :src="icon"></span>
  947. </div>
  948. [[module.name]]
  949. <div class="submodules" v-if="!hideSubmodules && module.modules && module.modules.length">
  950. <module v-for="submodule in module.modules" :module="submodule" draggable="true"></module>
  951. </div>
  952. </div>
  953. `
  954. })
  955. /**
  956. * A non-interactive Module Vue Component
  957. * Used for displaying the module
  958. */
  959. app.component('moduleDisplay', {
  960. delimiters: ['[[', ']]'],
  961. props: {
  962. module: {
  963. type: Object,
  964. required: true,
  965. }
  966. },
  967. data() {
  968. return {
  969. defaultIcon: '/assets/tabler_icons/circle-dotted.svg'
  970. }
  971. },
  972. computed: {
  973. icon() {
  974. return this.module.icon ? this.module.icon : this.defaultIcon;
  975. }
  976. },
  977. template: `
  978. <div
  979. class="module"
  980. .module="module"
  981. >
  982. <div class="module__icon-wrapper">
  983. <span class="module__icon"><img :src="icon"></span>
  984. </div>
  985. [[module.name]]
  986. <div class="submodules" v-if="module.modules && module.modules.length">
  987. <module-display v-for="submodule in module.modules" :module="submodule"></module-display>
  988. </div>
  989. </div>
  990. `
  991. })
  992. /**
  993. * The Module list Vue Component
  994. */
  995. app.component('moduleList', {
  996. delimiters: ['[[', ']]'],
  997. props: {
  998. module: {
  999. type: Object,
  1000. required: true,
  1001. },
  1002. hideIcon: {
  1003. type: Boolean,
  1004. default: false,
  1005. }
  1006. },
  1007. data() {
  1008. return {
  1009. defaultIcon: '/assets/tabler_icons/circle-dotted.svg'
  1010. }
  1011. },
  1012. computed: {
  1013. icon() {
  1014. return this.module.icon ? this.module.icon : this.defaultIcon;
  1015. }
  1016. },
  1017. template: `
  1018. <li class="module-list-item">
  1019. <span class="module__icon" v-if="!hideIcon"><img :src="icon"> </span><strong>[[module.name]]</strong>: [[module.summary]]
  1020. <span class="module__config">
  1021. <span v-for="(value, key) in module.config">
  1022. <br />[[key]]: [[value]]
  1023. </span>
  1024. </span>
  1025. <ul class="submodules" v-if="module.modules && module.modules.length">
  1026. <module-list v-for="submodule in module.modules" :module="submodule" :hide-icon="hideIcon"></module-list>
  1027. </ul>
  1028. </li>
  1029. `
  1030. })
  1031. /**
  1032. * A simple button Vue Component
  1033. */
  1034. app.component('vueButton', {
  1035. delimiters: ['[[', ']]'],
  1036. props: {
  1037. icon: {
  1038. type: String,
  1039. required: false,
  1040. default: false
  1041. },
  1042. loading: {
  1043. type: Boolean,
  1044. required: false,
  1045. default: false
  1046. }
  1047. },
  1048. computed: {
  1049. classList() {
  1050. return {
  1051. 'has-icon': this.icon,
  1052. 'is-loading': this.loading
  1053. };
  1054. },
  1055. activeIcon() {
  1056. return this.loading ? '/assets/tabler_icons/refresh.svg' : this.icon;
  1057. }
  1058. },
  1059. template: `
  1060. <button class="btn" :class="classList"><img :src="activeIcon" v-if="icon"> <slot>Click Here</slot></button>
  1061. `
  1062. })
  1063. /**
  1064. * A icon Vue Component
  1065. */
  1066. app.component('icon', {
  1067. delimiters: ['[[', ']]'],
  1068. props: {
  1069. icon: {
  1070. type: String,
  1071. required: true,
  1072. default: '/assets/tabler_icons/circle-dotted.svg'
  1073. }
  1074. },
  1075. template: `
  1076. <span class="icon"><img :src="icon"></span>
  1077. `
  1078. })
  1079. /**
  1080. * Mounts the app to the DOM element with the id 'app'
  1081. */
  1082. vm = app.mount('#app')
  1083. /**
  1084. * Legacy functions for displaying old rules
  1085. */
  1086. // Turns RuleBuilder contents into an output-ready nested array
  1087. // Returns empty array if no modules
  1088. function builderArray() {
  1089. var modules = document.getElementById("module-input").children;
  1090. // takes an array of children
  1091. // returns an array with all modules in the array, recursively nested
  1092. function iterateArray(childs) {
  1093. var moduleArray = [];
  1094. if (childs.length > 0) {
  1095. for (var i = 0; i < childs.length; i++) {
  1096. module = childs[i];
  1097. if (module.classList[0] == "module") {
  1098. var moduleName = module.children.item("module-name");
  1099. var moduleData = moduleName.title;
  1100. var moduleChilds = module.children;
  1101. moduleArray.push(
  1102. [stripHTML(moduleName.innerHTML),
  1103. stripHTML(moduleData),
  1104. iterateArray(moduleChilds)]);
  1105. }
  1106. }
  1107. }
  1108. return moduleArray;
  1109. } // end function
  1110. return iterateArray(modules);
  1111. }
  1112. // returns HTML version of Builder content
  1113. function displayBuilderHTML() {
  1114. var output = "";
  1115. var mainArray = builderArray();
  1116. function arrayHTML(thisArray) {
  1117. var thisOutput = "";
  1118. if (thisArray.length > 0) {
  1119. thisOutput += '<ul>\n';
  1120. for (var i = 0; i < thisArray.length; i++) {
  1121. var item = thisArray[i];
  1122. thisOutput += '<li><strong>' + item[0] + '</strong> ';
  1123. thisOutput += item[1] + '</li>\n';
  1124. if (item[2].length > 0) {
  1125. thisOutput += arrayHTML(item[2]);
  1126. }
  1127. }
  1128. thisOutput += '</ul>\n';
  1129. }
  1130. return thisOutput
  1131. }
  1132. if (mainArray.length > 0) {
  1133. output += '<div class="builder-list">\n';
  1134. output += arrayHTML(mainArray) + '\n</div>\n';
  1135. }
  1136. document.getElementById("builder-field").innerHTML = output;
  1137. }
  1138. // Removes all HTML content, replacing line break tags with newlines
  1139. function stripHTML(input) {
  1140. input = input.replace(/<br ?\/?>/ig, "\n").replace(/(<([^>]+)>)/ig, '');
  1141. return input;
  1142. }