12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088 |
- /**
- * 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: []
- },
- legacy: false,
- rID: false,
- loading: false,
- publishing: false,
- view: (global.rule) ? true : false,
- preview: (global.rule) ? true : false,
- template: (global.rule) ? true : false,
- steinAPI: 'https://api.steinhq.com/v1/storages/5e8b937ab88d3d04ae0816a5',
- // 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',
- },
- // 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
- );
- (async () => {
- var rule = [];
- // read values from all sheets
- await store.read('rules', { search: { ruleID: id } }).then(data => {
- // test if there's anything in data
- if (data.length > 0) {
- rule = data[0];
- }
- console.log(rule);
- });
- // 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) : []
- }
- /** Add name to <title> for v3+ rules */
- document.title = rule.name + " / CommunityRule"
-
- this.loading = false;
-
- })();
- },
- // editor methods =========================================================
- /**
- * 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);
- },
- // export and download methods =============================================
- /**
- * Handles click event for publishing the rule
- */
- handleClickPublish() {
- // Confirm user knows what they're getting into
- 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;
- const ruleID = new Date().getTime(); // TODO: allow for custom named IDs, check for uniqueness
- // 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);
- });
- },
- /**
- * 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;
- },
- /**
- * 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;
- }
|