123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320 |
- /**
- * The Vue application instance.
- * https://vuejs.org/guide/essentials/application.html
- * This is the root component
- */
- const app = Vue.createApp({
- /**
- * to prevent conflict with jekyll we change the
- * delimiters from {{ }} to [[ ]]
- */
- delimiters: ['[[', ']]'],
- /**
- * The data object of the root component.
- * These variables are available in the html template.
- */
- data() {
- return {
- // The rule object
- rule: global.rule || {
- ruleID: "",
- timestamp: "",
- icon: "",
- name: "",
- lineage: "",
- summary: "",
- config: {},
- creator: {
- name: "",
- url: "",
- },
- modules: [],
- latest_version: 1,
- list: "",
- deleted: 0,
- forked: 0,
- edit_history: []
- },
- legacy: false,
- rID: false,
- loading: false,
- publishing: false,
- edit: false,
- isOwner: false,
- isAdmin: false,
- view: (global.rule) ? true : false,
- preview: (global.rule) ? true : false,
- template: (global.rule) ? true : false,
- steinAPI: 'https://api.steinhq.com/v1/storages/5e8b937ab88d3d04ae0816a5',
- backendUrl: '/api/publish_rule',
- backendUrlrule: '/api/get_rule',
- backedDeleterule: '/api/delete_rule',
- adminMail: "medlab@colorado.edu",
- // TODO keep an array of past states for undo/redo
- history: [],
- // the data of the current module in the editor
- editor: {
- source: null,
- previousState: null,
- module: null,
- },
- // the module that is currently being dragged
- holding: false,
- // object of icons for easy access
- icons: {
- plus: '/assets/tabler_icons/circle-plus.svg',
- info: '/assets/tabler_icons/info-circle.svg',
- chevron: '/assets/tabler_icons/chevrons-down.svg',
- blank: '/assets/tabler_icons/circle-dotted.svg',
- view: '/assets/tabler_icons/eye.svg',
- edit: '/assets/tabler_icons/tool.svg',
- plus: '/assets/tabler_icons/plus.svg',
- minus: '/assets/tabler_icons/minus.svg',
- publish: '/assets/tabler_icons/cloud-upload.svg',
- download: '/assets/tabler_icons/download.svg',
- export: '/assets/tabler_icons/file-download.svg',
- upload: '/assets/tabler_icons/file-upload.svg',
- fork: '/assets/tabler_icons/git-fork.svg',
- delete: '/assets/tabler_icons/x.svg',
- edit: '/assets/tabler_icons/edit.svg',
- arrow: '/assets/tabler_icons/arrow-narrow-left.svg'
- },
- // icons available in the editor
- moduleIcons: {
- culture: '/assets/tabler_icons/palette.svg',
- decision: '/assets/tabler_icons/thumb-up.svg',
- process: '/assets/tabler_icons/rotate.svg',
- structure: '/assets/tabler_icons/building.svg',
- relationship: '/assets/tabler_icons/heart.svg',
- economic: '/assets/tabler_icons/coin.svg',
- legal: '/assets/tabler_icons/license.svg',
- map: '/assets/tabler_icons/map.svg',
- communications: '/assets/tabler_icons/microphone.svg',
- },
- // icons available for rules
- ruleIcons: {
- atom: '/assets/tabler_icons/atom.svg',
- bandage: '/assets/tabler_icons/bandage.svg',
- book: '/assets/tabler_icons/book.svg',
- box: '/assets/tabler_icons/box.svg',
- church: '/assets/tabler_icons/building-church.svg',
- store: '/assets/tabler_icons/building-store.svg',
- brush: '/assets/tabler_icons/brush.svg',
- car: '/assets/tabler_icons/car.svg',
- clock: '/assets/tabler_icons/clock.svg',
- cloud: '/assets/tabler_icons/cloud.svg',
- compass: '/assets/tabler_icons/compass.svg',
- game: '/assets/tabler_icons/device-gamepad.svg',
- flask: '/assets/tabler_icons/flask.svg',
- location: '/assets/tabler_icons/location.svg',
- moon: '/assets/tabler_icons/moon.svg',
- settings: '/assets/tabler_icons/settings.svg',
- shield: '/assets/tabler_icons/shield.svg',
- star: '/assets/tabler_icons/star.svg',
- tool: '/assets/tabler_icons/tool.svg',
- world: '/assets/tabler_icons/world.svg',
- },
- // the template for modules
- moduleTemplate: {
- moduleID: "",
- name: "",
- icon: "",
- summary: "",
- config: {},
- type: "",
- modules: []
- },
- // tracks the current module library tab that is open
- moduleLibrary: 'culture',
- moduleTypes: {
- // custom: {
- // question: 'Modules that you\'ve created.',
- // icon: '/assets/tabler_icons/circle-plus.svg',
- // open: true
- // },
- culture: {
- question: 'What are the core missions, values, and norms?',
- icon: '/assets/tabler_icons/palette.svg',
- open: true
- },
- decision: {
- question: 'Who can make decisions and how?',
- icon: '/assets/tabler_icons/thumb-up.svg',
- open: false
- },
- process: {
- question: 'How are policies implemented, and how do they evolve?',
- icon: '/assets/tabler_icons/rotate.svg ',
- open: false
- },
- structure: {
- question: 'What kinds of roles and internal entities are there?',
- icon: '/assets/tabler_icons/building.svg',
- open: false
- }
- },
- // array of modules that have been created by the user
- // TODO: implement custom modules
- customModules: [],
- // library of existing modules
- modules: global.modules,
- exports: {
- markdown: null,
- json: null,
- }
- }
- },
- /**
- * Vue provide passes data to other components.
- * https://vuejs.org/guide/components/provide-inject.html#provide-inject
- */
- provide() {
- return {
- editor: this.editor,
- icons: this.icons,
- }
- },
- created() {
- this.addToEditor(this.newModule());
- var urlParams = new URLSearchParams(window.location.search);
- if (urlParams.has('r')) {
- this.rID = urlParams.get('r');
- this.preview = true;
- this.view = true;
- this.fetchRule(this.rID);
- }
- },
- computed: {
- /**
- * Exports the current rule into a normalized format
- * Cleans up all submodules so they are ready for export
- * @returns {Object} a Rule object
- */
- ruleExport() {
- //TODO: test if icon is an absolute url and only add the global if it is not
- /**
- * Takes a module and recursively cleans it and all submodules up
- * @param {Object} module a module object
- * @returns
- */
- function cleanModules(module) {
- const newModule = {
- moduleID: module.moduleID,
- name: module.name,
- icon: (module.icon && !module.icon.includes('http')) ? global.url + module.icon : module.icon,
- summary: module.summary,
- config: module.config,
- type: module.type,
- modules: (module.modules) ? module.modules.map(cleanModules) : [],
- }
- return newModule;
- }
- return {
- ruleID: (this.rule.ruleID) ? this.rule.ruleID : this.slugify(this.rule.name),
- timestamp: this.timesString(),
- icon: (this.rule.icon && !this.rule.icon.includes('http')) ? global.url + this.rule.icon : this.rule.icon,
- name: this.rule.name,
- lineage: this.rule.lineage,
- summary: this.rule.summary,
- config: this.rule.config,
- creator: {
- name: this.rule.creator.name,
- url: this.rule.creator.url,
- },
- modules: this.rule.modules.map(cleanModules)
- }
- },
- /**
- * @returns {String} the current rule as a JSON string
- */
- json() {
- return JSON.stringify(this.ruleExport, null, 2);
- },
- /**
- * Creates an array of all moduleIDs in use
- * @returns {Array} an array of all module's (in the library and custom modules) moduleID
- */
- listModuleIds() {
- const modules = [...this.rule.modules, ...this.customModules];
- return modules.map(module => module.moduleID)
- },
- /**
- * @returns {Object} the current module in the editor
- */
- moduleInEditor() {
- return this.moduleEditor[0]
- },
- /**
- * Tests if the current module in the editor has been modified
- * against the editor.previousState object
- * @returns {Boolean} true if the module in the editor has been modified
- */
- //TODO: find a more accurate solution than just turning the object into a string
- editorHasEdits() {
- return this.editor.module && Object.entries(this.editor.module).toString() !== Object.entries(this.editor.previousState).toString();
- },
- },
- methods: {
- // module methods ===========================================================
- /**
- * @returns {Object} a new module object from the moduleTemplate
- */
- newModule() {
- return JSON.parse(JSON.stringify(this.moduleTemplate));
- },
- /**
- * spreads a source module into a new module from the moduleTemplate
- * @param {Object} sourceModule the module to copy
- * @param {Boolean} includeSubmodules whether to copy submodules or not
- * @returns
- */
- cloneModule(sourceModule, includeSubmodules) {
- let output = {
- ...this.moduleTemplate,
- ...sourceModule,
- //TODO: implement lineage pattern, same as the rule does
- source: sourceModule, // keep track of where this module came from
- };
- if (!includeSubmodules) output.modules = [];
- // clear configs
- document.getElementById("newConfigKey").value =
- "Configuration";
- document.getElementById("newConfigValue").value =
- "Value";
- // delete unnecessary properties
- delete output.content;
- delete output.readonly;
- // TODO: give module a unique id
- // if (output.moduleID) output.moduleID = this.getUniqueId(output.moduleID);
- return output;
- },
- /**
- * Handles the click event to copy a module
- * @param {Event} ev the click event
- */
- handleClickCopyModule(ev) {
- const clickTarget = this.getClosestModule(ev.target);
- if (!clickTarget) return;
- this.copyModule(clickTarget.module);
- },
- /**
- * Handles the click event to edit a module
- * @param {Event} ev the click event
- */
- handleClickEditModule(ev) {
- const clickTarget = this.getClosestModule(ev.target);
- if (!clickTarget) return;
- this.editModule(clickTarget.module);
- },
- /**
- * Copies a module to the editor
- * @param {Object} module the module to copy
- */
- copyModule(module) {
- this.copyToEditor(module);
- },
- /**
- * moves a module to the editor
- * @param {Object} module the module to edit
- */
- editModule(module) {
- this.addToEditor(module);
- },
- /**
- * add a module to another module (or to the rule by default) as a submodule
- * @param {Object} module to add
- * @param {Object} target a module or the rule Object where the module should be added as a submodule
- */
- addModule(module, target = this.rule) {
- target.modules.push(module);
- },
- /**
- * remove a module from another module (or from the rule)
- * recursively moves through all submodules in the target
- * removes ONLY the first instance of the module
- * @param {Object} module the module to remove from target
- * @param {Object} target the module or rule to remove the module from (defaults to rule)
- */
- removeModule(module, target = this.rule) {
- if (!this.moduleContains(module, target)) return; // if the module is not in the target, do nothing
- //
- target.modules.forEach((m, idx) => {
- if (m === module) {
- target.modules.splice(idx, 1);
- return;
- } else {
- this.removeModule(module, m);
- }
- });
- },
- /**
- * Deletes custom module from the customModules array and clears the editor
- * @param {Object} module the module to be deleted
- */
- deleteModule(module) {
- this.removeCustomModule(module);
- // TODO: only clear the editor if the module is present in the editor
- this.clearEditor();
- },
- /**
- * Handles the start drag event for a module
- * @param {Event} ev the drag event
- */
- startDragModule(ev) {
- const targetModule = this.getClosestModule(ev.target);
- if (!targetModule) return;
- const module = targetModule.module;
- ev.dataTransfer.setDragImage(targetModule, ev.offsetX, ev.offsetY);
- this.holding = { module };
- },
- /**
- * Handles the start drag event for a module
- * when the module is being rearranged within the rule
- * @param {Event} ev the drag event
- */
- rearrangeModule(ev) {
- const targetModule = this.getClosestModule(ev.target);
- if (!targetModule) return;
- const source = this.getClosestModule(targetModule.parentNode).module;
- const module = targetModule.module;
- ev.dataTransfer.setDragImage(targetModule, ev.offsetX, ev.offsetY);
- this.holding = {
- module,
- source,
- };
- },
- /**
- * Handles the dragend event for a module
- */
- endDragModule() {
- this.holding = false;
- },
- /**
- * Recursively searches modules and their submodules for a module
- * @param {Object} needle the module to search for
- * @param {Object} haystack the module to search in (defaults to rule)
- * @returns {Boolean} true if the module is in the haystack
- */
- // TODO: return the module location in the haystack (Maybe?)
- moduleContains(needle, haystack = this.rule) {
- if (!haystack.modules) return false; // does the haystack even have modules?
- if (haystack.modules.includes(needle)) return true; // is the needle in the haystack?
- return haystack.modules.some(submodule => this.moduleContains(needle, submodule)); // is the needle in any of the submodules?
- },
- // rule methods ===========================================================
- /**
- * Handles the drop event for a module
- * adds the module to the closest submodule or the rule depending on what it is dropped onto
- * then adds the module to the editor
- * @param {Event} ev the drop event
- */
- dropOnRule(ev) {
- //TODO browser drag objects that hover over drop zone are showing a 'add' icon
- const landingNode = this.getClosestModule(ev.target);
- if (!this.holding.module || !landingNode) return; // if there is no module to drop or no landing node, do nothing
- const landingModule = landingNode.module; // module is set with the v-bind prop binding
- const holdingModule = this.holding.module;
- if (holdingModule === landingModule) return; // if the module is the same, do nothing
- // if the module being dropped is readyonly clone it, otherwise use the original
- const readonly = holdingModule.readonly;
- const module = (readonly) ? this.cloneModule(holdingModule) : holdingModule;
- if (this.holding.source) {
- // if the module has a source, remove it from that source
- this.removeModule(holdingModule, this.holding.source);
- }
- this.addModule(module, landingModule);
- this.editModule(module);
- this.endDragModule();
- },
- fetchRule(id) {
- this.loading = true;
- // handle legacy links
- // TODO: handle this at a global level
- let redirect = {
- 'benevolent_dictator': 'benevolent-dictator',
- circles: 'circles',
- consensus: 'consensus',
- 'do-ocracy': 'do-ocracy',
- 'elected_board': 'elected-board',
- jury: 'jury',
- petition: 'petition',
- 'self-appointed_board': 'self-appointed-board',
- }
- // if the rule is a legacy link, redirect
- if (redirect[id]) {
- location.href = `/templates/${redirect[id]}`;
- return;
- }
- const store = new SteinStore(
- this.steinAPI
- );
- const backendUrlGetRule = this.backendUrlrule + "?ruleId=" + id;
- (async () => {
- var rule = [];
- // read values from all sheets
- await fetch(backendUrlGetRule, {
- method: 'GET',
- headers: {
- 'Content-Type': 'application/json',
- // Add any other headers you may need, such as authorization headers
- },
- }).then(response => {
- if (!response.ok) {
- throw new Error('Network response was not ok');
- }
- return response.json(); // This returns another Promise
- })
- .then(data => {
- if (data.rules.length > 0) {
- rule = JSON.parse(data.rules)[0];
- }
- });
- // no rule found, exit
- // TODO: inform the user that the rule was not found
- if (!rule) {
- this.loading = false;
- return;
- }
- // if this is a legacy (pre-v3) set it as such
- if (rule.version < 3) {
- this.loading = false;
- this.legacy = true;
- this.rule = rule;
- Vue.nextTick(() => {
- if (rule.version == 2) displayBuilderHTML();
- });
- return;
- }
- var resultArray = ''
- if (rule.list != null) {
- const splitArray = rule.list.split(':');
- resultArray = splitArray.filter(value => value.trim() !== '');
- }
- var forkedOrNot = 0
- if (resultArray.length > 1) {
- forkedOrNot = 1;
- }
- this.rule = {
- ruleID: rule.rule_id,
- timestamp: rule.time_stamp,
- icon: rule.icon,
- name: rule.name,
- lineage: rule.lineage,
- summary: rule.summary,
- config: rule.config,
- creator: {
- name: rule.creator_name,
- url: rule.creator_url,
- },
- modules: (rule.modules) ? JSON.parse(rule.modules) : [],
- latest_version: rule.latest_version,
- list: rule.list,
- forked: forkedOrNot,
- deleted: rule.deleted,
- edit_history: resultArray
- }
- if (rule.email != null && rule.email != undefined && rule.email == localStorage.getItem('userEmail')) {
- this.isOwner = true;
- this.isAdmin = true; // this will allow the user to delete his own rules
- }
- if (localStorage.getItem('userEmail') == this.adminMail) {
- this.isAdmin = true;
- }
- /** Add name to <title> for v3+ rules */
- document.title = rule.name + " / CommunityRule"
- this.loading = false;
- })();
- /*fetch(backendUrlGetRule, {
- method: 'GET',
- headers: {
- 'Content-Type': 'application/json',
- // Add any other headers you may need, such as authorization headers
- },
- }).then(response => {
- if (!response.ok) {
- throw new Error('Network response was not ok');
- }
- return response.json(); // This returns another Promise
- }).then (data => {
- var rule = [];
- if (data.rules.length > 0) {
- rule = data.rules;
- }
- // no rule found, exit
- // TODO: inform the user that the rule was not found
- if (!rule) {
- this.loading = false;
- return;
- }
- // if this is a legacy (pre-v3) set it as such
- if (rule.version < 3) {
- this.loading = false;
- this.legacy = true;
- this.rule = rule;
- Vue.nextTick(() => {
- if (rule.version == 2) displayBuilderHTML();
- });
- return;
- }
- this.rule = {
- ruleID: rule.ruleID,
- timestamp: rule.timestamp,
- icon: rule.icon,
- name: rule.name,
- lineage: rule.lineage,
- summary: rule.summary,
- config: rule.config,
- creator: {
- name: rule.creatorName,
- url: rule.creatorUrl,
- },
- modules: (rule.modules) ? JSON.parse(rule.modules) : []
- }
- document.title = rule.name + " / CommunityRule"
-
- this.loading = false;
- }); */
- },
- getRuleLink(ruleId) {
- // Further split each part based on '@'
- const subParts = ruleId.split('@');
- // Use the desired sub-part in your link construction
- // For example, if you want the first sub-part after splitting on '@'
- const finalPart = subParts[0];
- return `/create/?r=${finalPart}`;
- },
- getRuleName(ruleId) {
- // Further split each part based on '@'
- const subParts = ruleId.split('@');
- // Use the desired sub-part in your link construction
- // For example, if you want the first sub-part after splitting on '@'
- const ruleName = subParts[1];
- return ruleName
- },
- // editor methods =========================================================
- clickDelete() {
- (async () => {
- // read values from all sheets
- const requestData = {
- ruleID: this.rule.ruleID
- }
- await fetch(this.backedDeleterule, {
- method: 'DELETE',
- headers: {
- 'Content-Type': 'application/json',
- // Add any other headers you may need, such as authorization headers
- },
- body: JSON.stringify(requestData),
- }).then(response => {
- if (!response.ok) {
- alert("Error while deleting rule");
- throw new Error('Network response was not ok');
- }
- return response.json(); // This returns another Promise
- }).then(data => {
- alert('Rule deleted successfully');
- window.open("/library", false);
- });
- this.loading = false;
- })();
- },
- /**
- * Adds a module to the editor
- * @param {Object} module the module to add to the editor
- */
- addToEditor(module) {
- this.preventEditorLoss();
- this.setEditorSource(module);
- this.setEditorPreviousState(module);
- this.editor.module = module;
- },
- /**
- * Copies a module to the editor
- * @param {Object} module the module to copy to the editor
- */
- copyToEditor(module) {
- const moduleCopy = this.cloneModule(module);
- this.preventEditorLoss();
- this.setEditorSource(module);
- this.setEditorPreviousState(moduleCopy);
- this.editor.module = moduleCopy;
- },
- /**
- * Takes a module and clones it into the editor.previousState
- * @param {Object} module the module to add to the previous state
- */
- setEditorPreviousState(module) {
- this.editor.previousState = { ...module };
- },
- /**
- * Sets the editor.source to the module
- * @param {Object} module the module to set the editor source to
- */
- setEditorSource(module) {
- this.editor.source = module;
- },
- /**
- * Checks if the editor has edits and that the current module in the editor is not present in the rule
- * If the module in the editor would be lost, confirm with the user
- * then adds the module to the customModules array
- */
- preventEditorLoss() {
- // if the editor has changes and the module isn't in the rule, save it to the custom modules
- if (this.editorHasEdits && !this.moduleContains(this.editor.module)) {
- this.confirm('You have unsaved changes. Are you sure you want to discard them?')
- this.addCustomModule(this.editor.module);
- }
- },
- /**
- * Handles the click event for adding a module from the editor to the rule
- */
- clickAddModule() {
- const module = this.editor.module;
- this.addModule(module);
- this.addToEditor(module);
- },
- /**
- * Handles the click event for removing a module in the editor from the rule
- */
- clickRemoveModule() {
- const module = this.editor.module;
- this.removeModule(module);
- },
- /**
- * Clears the editor
- */
- clearEditor() {
- this.preventEditorLoss();
- this.editor.module = null;
- this.editor.previousState = null;
- },
- /**
- * Saves the module in the editor to customModules array
- */
- saveEditor() {
- this.addCustomModule(this.editor.module);
- this.setEditorPreviousState(this.editor.module);
- },
- // config methods =========================================================
- /**
- * Add custom config entry to the module
- */
- addConfig() {
- const k = document.getElementById("newConfigKey").value;
- const v = document.getElementById("newConfigValue").value;
- this.editor.module.config[k] = v;
- this.resetNewConfigInputs();
- },
- /**
- * Removes the config entry from the module
- */
- removeConfig(key) {
- delete this.editor.module.config[key];
- },
- // custom module methods ==================================================
- /**
- * Adds a module to the customModules array
- * @param {Object} module the module to add to the customModules array
- */
- addCustomModule(module) {
- // if module is not in customModules, add it
- if (!this.customModules.includes(module)) {
- this.customModules.unshift(module);
- }
- },
- /**
- * Creates a new module, sets a default name, and adds it to the editor
- */
- newCustomModule() {
- const module = this.newModule();
- module.name = 'New Module';
- module.config = {};
- this.resetNewConfigInputs();
- this.addToEditor(module);
- },
- resetNewConfigInputs() {
- document.getElementById("newConfigKey").value = "Configuration";
- document.getElementById("newConfigValue").value = "Value";
- },
- /**
- * Removes a module from the customModules array
- * @param {Object} module the module to remove from the customModules array
- */
- removeCustomModule(module) {
- this.confirm("are you sure you want to remove this custom module?");
- const index = this.customModules.indexOf(module);
- this.customModules.splice(index, 1);
- },
- /**
- * Handles confirmation messages for users
- * @param {String} msg the message to display in the confirm dialog
- */
- // TODO: add a confirm dialog and return boolean based on user input
- confirm(msg) {
- console.log(msg);
- },
- /**
- * Handles click event for publishing the rule
- */
- handleClickPublish() {
- // Confirm user knows what they're getting into
- if (!localStorage.getItem('userEmail')) {
- alert('Please login first to publish this rule')
- return;
- }
- if (!confirm("Publish to the public Library?")) return;
- if (!this.rule.name) {
- alert("Please enter a name for this rule.");
- return;
- }
- if (this.rule.modules.length < 1) {
- alert("Please add at least one module to this rule.");
- return;
- }
- this.publishing = true;
- const rule = this.ruleExport;
- var ruleID = new Date().getTime(); // TODO: allow for custom named IDs, check for uniqueness
- if (this.edit) {
- ruleID = this.rule.ruleID;
- }
- var latest_ver = 1;
- if (this.edit) {
- latest_ver = parseInt(this.rule.latest_version) + 1;
- }
- var previousListVersion = "";
- if (this.edit) {
- previousListVersion = this.rule.list
- } else {
- previousListVersion = ruleID + "@" + rule.name + ":" + this.rule.list;
- }
- // ------------------ exisituing code ---------------
- // add to database
- // const store = new SteinStore(
- // this.steinAPI
- // );
- // store.append('rules', [{
- // ruleID: ruleID,
- // timestamp: rule.timestamp,
- // icon: rule.icon,
- // name: rule.name,
- // lineage: rule.lineage,
- // summary: rule.summary,
- // config: this.jsonify(rule.config),
- // modules: this.jsonify(rule.modules),
- // creatorName: rule.creator.name,
- // creatorUrl: rule.creator.url,
- // version: 3
- // }]).then(data => {
- // this.publishing = false;
- // window.open("/create/?r=" + ruleID, "_self", false);
- // });
- // -=---------------------------- updated code -------------------
- const requestData = {
- ruleID: ruleID,
- timestamp: rule.timestamp,
- icon: rule.icon,
- name: rule.name,
- lineage: rule.lineage,
- summary: rule.summary,
- config: this.jsonify(rule.config),
- modules: this.jsonify(rule.modules),
- creatorName: rule.creator.name,
- creatorUrl: rule.creator.url,
- email: localStorage.getItem('userEmail'),
- edit: this.edit,
- version: 3,
- latest_version: latest_ver,
- list: previousListVersion
- }
- fetch(this.backendUrl, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'Access-Control-Allow-Methods': 'POST',
- 'Access-Control-Allow-Headers': 'Content-Type'
- // Add any other headers you may need, such as authorization headers
- },
- body: JSON.stringify(requestData),
- }).then(response => {
- if (!response.ok) {
- throw new Error(`HTTP error! Status: ${response.status}`);
- }
- return response.json(); // or response.text() if expecting plain text
- }).then(data => {
- // Handle the data received from the backend
- }).then(data => {
- this.publishing = false;
- window.open("/create/?r=" + ruleID, "_self", false);
- }).catch(error => {
- // Handle errors that occurred during the fetch
- console.error('Fetch error:', error);
- });
- },
- /**
- * Handles the click event for downloading the rule as a Markdown file
- * Creates a YAML string of the rule
- * Then adds it to the bottom of a markdown file
- * created from the #rule-export html element
- */
- handleClickDownload() {
- const yaml = jsyaml.dump(this.ruleExport);
- const turndown = new TurndownService();
- const html = document.getElementById('rule-export');
- if (!html) return;
- const markdown = turndown.turndown(html);
- const output = markdown + '\n\n----\n```yaml\n' + yaml + '\n```';
- this.saveFile(`${this.ruleExport.ruleID}.md`, output, 'text/markdown');
- },
- /**
- * IE10+ Firefox, and Chrome method for saving a file
- * https://stackoverflow.com/a/33542499
- * @param {String} filename name of the file to save
- * @param {String} data data to save into a file
- * @param {String} type MIME type of the file
- */
- saveFile(filename, data, type) {
- const blob = new Blob([data], { type: type });
- if (window.navigator.msSaveOrOpenBlob) {
- window.navigator.msSaveBlob(blob, filename);
- }
- else {
- const elem = window.document.createElement('a');
- elem.href = window.URL.createObjectURL(blob);
- elem.download = filename;
- document.body.appendChild(elem);
- elem.click();
- document.body.removeChild(elem);
- }
- },
- /**
- * Handles the click event for importing a rule from a JSON file
- */
- handleClickImport() {
- },
- // utility methods ===========================================================
- /**
- * Takes an html element and finds the closest node (inclusive) that has a data-module-id attribute
- * @param {Node} el the html element to check
- * @returns {Node} the closest node with a data-module-id attribute
- */
- getClosestModule(el) {
- const parent = el.closest('[data-module-id]');
- if (!parent) return false;
- if (!parent.module) return false;
- return parent;
- },
- /**
- * Handles the click event for activating the rule preview
- */
- clickPreview() {
- if (this.template) this.rule.icon = ''; // TODO: find a less hacky way to reset template icons
- this.view = false;
- this.preview = !this.preview;
- },
- /**
- * Handles the click event for activating the rule preview for edit rule
- */
- clickPreviewEdit() {
- if (this.template) this.rule.icon = ''; // TODO: find a less hacky way to reset template icons
- this.view = false;
- this.preview = !this.preview;
- this.edit = true;
- },
- /**
- * Filters module library based on the search term
- * @param {String} type the name of the type to filter
- * @returns {Array} the filtered module library
- */
- getModulesByType(type) {
- return this.modules.filter(module => module.type === type)
- },
- /**
- * Slugifies a string
- * https://gist.github.com/codeguy/6684588 (one of the comments)
- * @param {String} string the string to slugify
- * @returns
- */
- slugify(string) {
- const separator = '-';
- return string
- .toString()
- .normalize('NFD') // split an accented letter in the base letter and the accent
- .replace(/[\u0300-\u036f]/g, '') // remove all previously split accents
- .toLowerCase()
- .replace(/[^a-z0-9 -]/g, '') // remove all chars not letters, numbers and spaces (to be replaced)
- .trim()
- .replace(/\s+/g, separator);
- },
- /**
- * Creates a human readable timestamp string
- * @param {String} date optional date to format
- * @returns {String} human readable date '2022.4.12 14:44:56 UTC'
- */
- timesString(date) {
- let now = new Date(date);
- if (isNaN(now)) now = new Date();
- return now.getUTCFullYear() + '.' + (now.getUTCMonth() + 1) + '.' + now.getUTCDate()
- + ' ' + now.getUTCHours() + ":" + now.getUTCMinutes() + ":" + now.getUTCSeconds()
- + ' UTC';
- },
- /**
- * stringify an Object
- * @param {Object} obj
- * @returns JSON string
- */
- jsonify(obj) {
- return JSON.stringify(obj, null, 2);
- },
- /**
- * Takes a moduleID and checks if that moduleID is in use
- * if so, returns the moduleID with a number appended to it
- * @param {String} test the moduleID to test
- * @returns {String} the moduleID, or if in use, with a number appended to it
- */
- getUniqueId(test) {
- let id = test;
- let i = 0;
- while (this.listModuleIds.includes(id)) {
- i++;
- id = `${test}-${i}`;
- }
- return id
- },
- },
- });
- /**
- * The Module Vue Component
- */
- app.component('module', {
- delimiters: ['[[', ']]'],
- inject: ['editor'],
- props: {
- module: {
- type: Object,
- required: true,
- },
- inEditor: {
- type: Boolean,
- default: false,
- },
- hideSubmodules: {
- type: Boolean,
- default: false,
- }
- },
- data() {
- return {
- defaultIcon: '/assets/tabler_icons/circle-dotted.svg',
- mouseOver: false,
- dragOver: false
- }
- },
- computed: {
- icon() {
- return this.module.icon ? this.module.icon : this.defaultIcon;
- },
- moduleClass() {
- return {
- 'in-editor': this.editor.source == this.module,
- 'mouse-over': this.mouseOver,
- // TODO: when dragging over the icon the drag-over class disappears
- 'drag-over': this.dragOver
- }
- }
- },
- template: `
- <div
- class="module"
- :class="moduleClass"
- .module="module"
- @mouseover.stop="this.mouseOver = true"
- @mouseout="this.mouseOver = false"
- @dragenter.self="this.dragOver = true"
- @dragleave.self="this.dragOver = false"
- @drop.self="this.dragOver = false"
- :data-module-id="module.moduleID"
- >
- <div class="module__icon-wrapper">
- <span class="module__grain"><img src="/assets/tabler_icons/grain.svg"></span>
- <span class="module__icon"><img :src="icon"></span>
- </div>
- [[module.name]]
- <div class="submodules" v-if="!hideSubmodules && module.modules && module.modules.length">
- <module v-for="submodule in module.modules" :module="submodule" draggable="true"></module>
- </div>
- </div>
- `
- })
- /**
- * A non-interactive Module Vue Component
- * Used for displaying the module
- */
- app.component('moduleDisplay', {
- delimiters: ['[[', ']]'],
- props: {
- module: {
- type: Object,
- required: true,
- }
- },
- data() {
- return {
- defaultIcon: '/assets/tabler_icons/circle-dotted.svg'
- }
- },
- computed: {
- icon() {
- return this.module.icon ? this.module.icon : this.defaultIcon;
- }
- },
- template: `
- <div
- class="module"
- .module="module"
- >
- <div class="module__icon-wrapper">
- <span class="module__icon"><img :src="icon"></span>
- </div>
- [[module.name]]
- <div class="submodules" v-if="module.modules && module.modules.length">
- <module-display v-for="submodule in module.modules" :module="submodule"></module-display>
- </div>
- </div>
- `
- })
- /**
- * The Module list Vue Component
- */
- app.component('moduleList', {
- delimiters: ['[[', ']]'],
- props: {
- module: {
- type: Object,
- required: true,
- },
- hideIcon: {
- type: Boolean,
- default: false,
- }
- },
- data() {
- return {
- defaultIcon: '/assets/tabler_icons/circle-dotted.svg'
- }
- },
- computed: {
- icon() {
- return this.module.icon ? this.module.icon : this.defaultIcon;
- }
- },
- template: `
- <li class="module-list-item">
- <span class="module__icon" v-if="!hideIcon"><img :src="icon"> </span><strong>[[module.name]]</strong>: [[module.summary]]
- <span class="module__config">
- <span v-for="(value, key) in module.config">
- <br />[[key]]: [[value]]
- </span>
- </span>
- <ul class="submodules" v-if="module.modules && module.modules.length">
- <module-list v-for="submodule in module.modules" :module="submodule" :hide-icon="hideIcon"></module-list>
- </ul>
- </li>
- `
- })
- /**
- * A simple button Vue Component
- */
- app.component('vueButton', {
- delimiters: ['[[', ']]'],
- props: {
- icon: {
- type: String,
- required: false,
- default: false
- },
- loading: {
- type: Boolean,
- required: false,
- default: false
- }
- },
- computed: {
- classList() {
- return {
- 'has-icon': this.icon,
- 'is-loading': this.loading
- };
- },
- activeIcon() {
- return this.loading ? '/assets/tabler_icons/refresh.svg' : this.icon;
- }
- },
- template: `
- <button class="btn" :class="classList"><img :src="activeIcon" v-if="icon"> <slot>Click Here</slot></button>
- `
- })
- /**
- * A icon Vue Component
- */
- app.component('icon', {
- delimiters: ['[[', ']]'],
- props: {
- icon: {
- type: String,
- required: true,
- default: '/assets/tabler_icons/circle-dotted.svg'
- }
- },
- template: `
- <span class="icon"><img :src="icon"></span>
- `
- })
- /**
- * Mounts the app to the DOM element with the id 'app'
- */
- vm = app.mount('#app')
- /**
- * Legacy functions for displaying old rules
- */
- // Turns RuleBuilder contents into an output-ready nested array
- // Returns empty array if no modules
- function builderArray() {
- var modules = document.getElementById("module-input").children;
- // takes an array of children
- // returns an array with all modules in the array, recursively nested
- function iterateArray(childs) {
- var moduleArray = [];
- if (childs.length > 0) {
- for (var i = 0; i < childs.length; i++) {
- module = childs[i];
- if (module.classList[0] == "module") {
- var moduleName = module.children.item("module-name");
- var moduleData = moduleName.title;
- var moduleChilds = module.children;
- moduleArray.push(
- [stripHTML(moduleName.innerHTML),
- stripHTML(moduleData),
- iterateArray(moduleChilds)]);
- }
- }
- }
- return moduleArray;
- } // end function
- return iterateArray(modules);
- }
- // returns HTML version of Builder content
- function displayBuilderHTML() {
- var output = "";
- var mainArray = builderArray();
- function arrayHTML(thisArray) {
- var thisOutput = "";
- if (thisArray.length > 0) {
- thisOutput += '<ul>\n';
- for (var i = 0; i < thisArray.length; i++) {
- var item = thisArray[i];
- thisOutput += '<li><strong>' + item[0] + '</strong> ';
- thisOutput += item[1] + '</li>\n';
- if (item[2].length > 0) {
- thisOutput += arrayHTML(item[2]);
- }
- }
- thisOutput += '</ul>\n';
- }
- return thisOutput
- }
- if (mainArray.length > 0) {
- output += '<div class="builder-list">\n';
- output += arrayHTML(mainArray) + '\n</div>\n';
- }
- document.getElementById("builder-field").innerHTML = output;
- }
- // Removes all HTML content, replacing line break tags with newlines
- function stripHTML(input) {
- input = input.replace(/<br ?\/?>/ig, "\n").replace(/(<([^>]+)>)/ig, '');
- return input;
- }
|