/**
* 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: "communityrule@medlab.host",
// 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 == 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
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: `