123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212 |
- /**
- * 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 {
- // state of the editor; used for undo
- state: {
- // The rule object
- rule: global.rule || {
- ruleID: "",
- timestamp: "",
- icon: "",
- name: "",
- lineage: "",
- summary: "",
- config: {},
- creator: {
- name: "",
- url: "",
- },
- modules: []
- },
- // the data of the current module in the editor
- editor: {
- source: null,
- previousState: null,
- module: null,
- },
- },
- 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',
- // keep an array of past states for undo/redo
- currentStateIndex: -1,
- history: [],
- // 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,
- }
- }
- },
- // on load
- mounted() {
- // put initial state into this.history
- this.initializeUndoFunctionality()
- },
- /**
- * Vue provide passes data to other components.
- * https://vuejs.org/guide/components/provide-inject.html#provide-inject
- */
- provide() {
- return {
- editor: this.state.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.state.rule.ruleID) ? this.state.rule.ruleID : this.slugify(this.state.rule.name),
- timestamp: this.timesString(),
- icon: (this.state.rule.icon && !this.state.rule.icon.includes('http')) ? global.url + this.state.rule.icon : this.state.rule.icon,
- name: this.state.rule.name,
- lineage: this.state.rule.lineage,
- summary: this.state.rule.summary,
- config: this.state.rule.config,
- creator: {
- name: this.state.rule.creator.name,
- url: this.state.rule.creator.url,
- },
- modules: this.state.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.state.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.state.editor.module && Object.entries(this.state.editor.module).toString() !== Object.entries(this.state.editor.previousState).toString();
- },
- /**
- * required to update editor on state updates
- */
- computedState() {
- // state of the editor; used for undo
- return this.state;
- }
- },
- 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.state.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.state.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.state.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.state.rule = rule;
- Vue.nextTick(() => {
- if (rule.version == 2) displayBuilderHTML();
- });
- return;
- }
- this.state.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.state.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.state.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.state.editor.previousState = { ...module };
- },
- /**
- * Sets the editor.source to the module
- * @param {Object} module the module to set the editor source to
- */
- setEditorSource(module) {
- this.state.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.state.editorHasEdits && !this.moduleContains(this.state.editor.module)) {
- this.confirm('You have unsaved changes. Are you sure you want to discard them?')
- this.addCustomModule(this.state.editor.module);
- }
- },
- /**
- * Handles the click event for adding a module from the editor to the rule
- */
- clickAddModule() {
- const module = this.state.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.state.editor.module;
- this.removeModule(module);
- },
- /**
- * Clears the editor
- */
- clearEditor() {
- this.preventEditorLoss();
- this.state.editor.module = null;
- this.state.editor.previousState = null;
- },
- /**
- * Saves the module in the editor to customModules array
- */
- saveEditor() {
- this.addCustomModule(this.state.editor.module);
- this.setEditorPreviousState(this.state.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.state.editor.module.config[k] = v;
- this.resetNewConfigInputs();
- },
-
- /**
- * Removes the config entry from the module
- */
- removeConfig(key) {
- delete this.state.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.state.rule.name ) {
- alert("Please enter a name for this rule.");
- return;
- }
- if ( this.state.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() {
- },
- handleStateUpdate()
- {
- const MAX_UNDOS_ALLOWED = 15;
- this.currentStateIndex += 1;
- this.history[this.currentStateIndex] = JSON.parse(JSON.stringify(this.state));
- // if we have >1 states in the redo stack (i.e. history past our current index)
- if (this.currentStateIndex != this.history.length - 1)
- {
- // truncate history, so we can't redo out of order.
- this.history.length = this.currentStateIndex + 1; // truncates forked redo states
- }
- while (this.history.length > MAX_UNDOS_ALLOWED)
- {
- this.history.shift();
- this.currentStateIndex -= 1;
- }
- },
- canUndo()
- {
- return this.currentStateIndex > 0;
- },
- /**
- * handles undo by shifting the state back one index in the history
- */
- handleUndo()
- {
- this.appComponent.focus();
- if (this.canUndo())
- {
- this.currentStateIndex -= 1;
- this.state = this.history[this.currentStateIndex];
- }
- else
- {
- console.warn('No more actions to undo!')
- }
- },
- canRedo()
- {
- return this.currentStateIndex < this.history.length - 1;
- },
- /**
- * handles redo by shifting the currentStateIndex forward
- */
- handleRedo()
- {
- this.appComponent.focus();
- if (this.canRedo())
- {
- this.currentStateIndex += 1;
- this.state = this.history[this.currentStateIndex]
- }
- else
- {
- console.warn('No more actions to redo!')
- }
- },
- /**
- * initializes undo capabilities
- */
- initializeUndoFunctionality()
- {
- this.appComponent = document.getElementById('app'); // todo this doesn't work yet
- this.handleStateUpdate();
- const component = this;
- document.addEventListener('keydown', function (event)
- {
- console.log('keypress event:')
- console.log(event)
- console.log('ctrl?', event.ctrlKey)
- if (component.getModifierKeyByOs(event) && event.key === 'z')
- {
- event.preventDefault();
- if (event.shiftKey)
- {
- component.handleRedo();
- }
- else
- {
- component.handleUndo();
- }
- }
- });
- },
- /**
- * the modifier key is different for mac vs windows.
- * this should (but does not yet) return
- * event.ctrlKey if os == windows
- * event.metaKey if os == mac
- * @param {*} event
- * @returns bool: true if the os-specific modifier key is depressed
- */
- getModifierKeyByOs(event) {
- // TODO: this allows ctrl+z on a mac to undo, which isn't normal.
- // it might also allow windows key + z to undo on windows.
- return (event.metaKey || event.ctrlKey)
- },
- // 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.state.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;
- }
|