├── LICENSE ├── NOTICE ├── README.md ├── actions ├── ArrayActions.js ├── ContextActions.js ├── CustomActions.js ├── DebugActions.js ├── FlowActions.js ├── MathActions.js ├── PropertyActions.js ├── TextActions.js ├── TimingActions.js └── descriptor.json ├── codestrates ├── ConceptDefinitionFragment.js ├── blockly-editor │ ├── SVGIconButtonField.js │ ├── blockly-editor.js │ ├── blockly-editor.scss │ ├── blockly_editor_plus.svg │ └── descriptor.json ├── descriptor.json └── whenv2-schema.json ├── core ├── Action.js ├── Behaviour.js ├── Concept.js ├── ConceptLoader.js ├── Datastore.js ├── DirectDatastore.js ├── Filter.js ├── Property.js ├── PropertyCache.js ├── Trigger.js ├── VarvEngine.js ├── VarvPerformance.js ├── YAMLJSONConverter.js └── descriptor.json ├── datastores ├── dom │ ├── DOMDataStore.js │ └── descriptor.json ├── giga │ ├── GigaVarvDatastore.js │ └── descriptor.json ├── localstorage │ ├── LocalStorageDataStore.js │ └── descriptor.json ├── location │ ├── LocationDataStore.js │ └── descriptor.json ├── memory │ ├── MemoryDataStore.js │ └── descriptor.json ├── signaling │ ├── SignalingDataStore.js │ └── descriptor.json ├── sql │ ├── SQLDataStore.js │ ├── SQLFilter.js │ └── descriptor.json └── wsdata │ ├── WSDataDataStore.js │ └── descriptor.json ├── descriptor.json ├── integration ├── audio-router │ ├── AudioRouterFragment.js │ ├── AudioRouterGUIEditor.js │ ├── audio-router.js │ ├── descriptor.json │ ├── main.scss │ └── templates.html ├── cauldron-delayloader │ ├── cauldron-delayloader.js │ └── descriptor.json ├── cauldron │ ├── CauldronDatastore.js │ ├── ConceptDecorator.js │ ├── ConceptMenuActions.js │ ├── ConceptTreeGenerator.js │ ├── InspectorConceptBinding.js │ ├── base.scss │ └── descriptor.json └── varvscript │ ├── VarvScriptFragment.js │ ├── descriptor.json │ └── varvscript.js ├── jsdoc.json ├── prototypes ├── README.md ├── varv.html └── varv.zip ├── repository.html ├── triggers ├── FlowTriggers.js ├── PropertyTriggers.js ├── TimingTriggers.js └── descriptor.json ├── varv.html └── views └── dom ├── dom-highlight ├── DOMHighlight.js └── descriptor.json ├── domdiff-view ├── DOMView.js ├── ParseNode.js ├── UpdatingEvalutation.js ├── ViewParticle.js ├── bindings │ ├── ConceptInstanceBinding.js │ ├── PropertyArrayEntryBinding.js │ ├── PropertyBinding.js │ ├── RuntimeExceptionBinding.js │ ├── TemplateBinding.js │ └── ValueBinding.js ├── descriptor.json └── parsetree │ ├── ElementParseNode.js │ ├── QueryParseNode.js │ ├── RootParseNode.js │ ├── ScopedParseNode.js │ ├── TemplateRefParseNode.js │ ├── TextParseNode.js │ └── YotaParseNode.js ├── domtriggers ├── DOMTriggers.js └── descriptor.json ├── domview-legacy ├── DOMView.js ├── descriptor.json └── domview.scss ├── inspector ├── Inspector.js ├── descriptor.json └── inspector.css └── react ├── JSXQueryParseNode.js ├── VarvReact.fragment └── descriptor.json /LICENSE: -------------------------------------------------------------------------------- 1 | Varv 2 | 3 | This code is licensed under the MIT License (MIT). 4 | 5 | Copyright 2020, 2021, 2022 Rolf Bagge, Janus B. Kristensen, CAVI, 6 | Center for Advanced Visualization and Interaction, Aarhus University 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the “Software”), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in 16 | all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Varv Licensing 2 | 3 | While licensed under MIT, Varv builds upon other pieces of work that may be 4 | licensed under other MIT-compatible open licenses. Please refer to the 5 | individual module licenses when building upon or extending Varv. 6 | This includes, but is not limited to, artwork covered by various Creative Commons 7 | CC0 and CC-BY licenses as well as some modules covered by the Apache 2.0 license. 8 | 9 | Varv was developed at CAVI - Center for Advanced Visualization and Interaction 10 | at Aarhus University in relation with research done by Marcel Borowski and 11 | Clemens Klokmose. 12 | 13 | Consider referencing their work if you use this in your research. 14 | See http://webstrates.net/ for more info. 15 | 16 | You should have received a copy of the MIT license alongside this code. 17 | If not, please refer to the following URL: 18 | https://mit-license.org/ 19 | 20 | Derivative works must reproduce this notice -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Varv - A piece of reprogrammable interactive software based on declarative data structures 2 | 3 | -------------------------------------------------------------------------------- /actions/CustomActions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CustomActions - Actions that allow custom code 3 | * 4 | * This code is licensed under the MIT License (MIT). 5 | * 6 | * Copyright 2020, 2021, 2022 Rolf Bagge, Janus B. Kristensen, CAVI, 7 | * Center for Advanced Visualization and Interaction, Aarhus University 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the “Software”), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in 17 | * all copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | * THE SOFTWARE. 26 | * 27 | */ 28 | 29 | /** 30 | * Actions that handle custom code 31 | * @namespace CustomActions 32 | */ 33 | 34 | /** 35 | * An action 'customJS' that can run a custom piece of javascript code on the current context, the function must be a function on the window object 36 | * @memberOf CustomActions 37 | * @example 38 | * //Runs window.myFunction 39 | * { 40 | * "customJS": { 41 | * "functionName": "myFunction" 42 | * } 43 | * } 44 | * 45 | * //Shorthand, runs window.myFunction 46 | * { 47 | * "customJS": "myFunction" 48 | * } 49 | */ 50 | class CustomJSAction extends Action { 51 | constructor(name, options, concept) { 52 | if(typeof options === "string") { 53 | options = { 54 | "functionName": options 55 | } 56 | } 57 | 58 | super(name, options, concept); 59 | } 60 | 61 | async apply(contexts, actionArguments) { 62 | return this.forEachContext(contexts, actionArguments, async (context, options)=> { 63 | if(this.options.functionName == null) { 64 | throw new Error("'functionName' must be set for action 'customJS'"); 65 | } 66 | 67 | let f = window[options.functionName]; 68 | 69 | if(f == null) { 70 | throw new Error("'window."+options.functionName+"' is not defined"); 71 | } 72 | 73 | if(typeof f !== "function") { 74 | throw new Error("'window."+options.functionName+"' is not a function"); 75 | } 76 | 77 | return await f(context, options); 78 | }); 79 | } 80 | } 81 | Action.registerPrimitiveAction("customJS", CustomJSAction); 82 | window.CustomJSAction = CustomJSAction; 83 | -------------------------------------------------------------------------------- /actions/TimingActions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TimingActions - Actions related to time 3 | * 4 | * This code is licensed under the MIT License (MIT). 5 | * 6 | * Copyright 2020, 2021, 2022 Rolf Bagge, Janus B. Kristensen, CAVI, 7 | * Center for Advanced Visualization and Interaction, Aarhus University 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the “Software”), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in 17 | * all copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | * THE SOFTWARE. 26 | * 27 | */ 28 | 29 | /** 30 | * Actions that deal with time 31 | * @namespace TimingActions 32 | */ 33 | 34 | /** 35 | * An action "wait" that waits for a given duration before continuing 36 | * @memberOf TimingActions 37 | * @example 38 | * //Wait for 100ms 39 | * { 40 | * "wait": { 41 | * "duration": 100 42 | * } 43 | * } 44 | * 45 | * //Wait for 100ms (shorthand version) 46 | * { 47 | * "wait": 100 48 | * } 49 | */ 50 | class WaitAction extends Action { 51 | static options() { 52 | return { 53 | "duration": "number" 54 | } 55 | } 56 | 57 | constructor(name, options, concept) { 58 | //shorthand 59 | if(typeof options === "number") { 60 | options = { 61 | "duration": options 62 | } 63 | } 64 | 65 | super(name, options, concept); 66 | } 67 | 68 | async apply(contexts, actionArguments) { 69 | if(this.options.duration == null) { 70 | throw new Error("Option 'duration' must be present on 'wait' action"); 71 | } 72 | return this.forEachContext(contexts, actionArguments, async (context, options)=>{ 73 | await new Promise((resolve)=>{ 74 | setTimeout(()=>{ 75 | resolve(); 76 | }, options.duration); 77 | }) 78 | 79 | return context; 80 | }); 81 | } 82 | } 83 | Action.registerPrimitiveAction("wait", WaitAction); 84 | window.WaitAction = WaitAction; 85 | -------------------------------------------------------------------------------- /actions/descriptor.json: -------------------------------------------------------------------------------- 1 | { 2 | "friendlyName": "Varv Standard Actions", 3 | "description": "Simple built-in actions for varv", 4 | "dependencies": [ 5 | "#varv-engine", 6 | "wpm_js_libs #mathjs" 7 | ], 8 | "assets": [], 9 | "license": "MIT", 10 | "version": "0.1", 11 | "changelog": {} 12 | } 13 | -------------------------------------------------------------------------------- /codestrates/ConceptDefinitionFragment.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ConceptDefinitionFragment - A Codestrate Fragment type for Concept Definitions 3 | * 4 | * This code is licensed under the MIT License (MIT). 5 | * 6 | * Copyright 2020, 2021, 2022 Rolf Bagge, Janus B. Kristensen, CAVI, 7 | * Center for Advanced Visualization and Interaction, Aarhus University 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the “Software”), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in 17 | * all copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | * THE SOFTWARE. 26 | * 27 | */ 28 | 29 | wpm.onRemoved(()=>{ 30 | Fragment.unRegisterFragmentType(ConceptDefinitionFragment); 31 | }); 32 | /** 33 | * A fragment that contains Varv code 34 | * 35 | * Supports auto - executes require() on load 36 | * @extends Fragments.Fragment 37 | * @hideconstructor 38 | * @memberof Fragments 39 | */ 40 | class ConceptDefinitionFragment extends Fragment { 41 | constructor(html) { 42 | super(html); 43 | } 44 | 45 | async require(options = {}) { 46 | return YAMLJSONConverter.loadFromString(this.raw).obj; 47 | } 48 | 49 | supportsAuto() { 50 | return true; 51 | } 52 | 53 | static type() { 54 | return "text/varv"; 55 | } 56 | }; 57 | window.ConceptDefinitionFragment = ConceptDefinitionFragment; 58 | Fragment.registerFragmentType(ConceptDefinitionFragment); 59 | 60 | function detectYAMLJSON(editor) { 61 | let code = editor.getModel().getValue().trim(); 62 | let numCurlyBrackets = (code.match(/{/g)||[]).length; 63 | 64 | let YAML = 0; 65 | let JSON = 0 66 | 67 | if(code.length === 0) { 68 | //If fragment is empty, set it up as JSON 69 | JSON += 4; 70 | } 71 | 72 | if(numCurlyBrackets > 10) { 73 | //Many curly brackets, probably JSON 74 | JSON++; 75 | } else { 76 | YAML++; 77 | } 78 | 79 | if(code.startsWith("{")) { 80 | //Code starts with a curly bracket, probably JSON 81 | JSON++; 82 | } else { 83 | YAML++; 84 | } 85 | 86 | if(code.endsWith("}")) { 87 | //Code ends with a curly bracket, probably JSON 88 | JSON++; 89 | } else { 90 | YAML++; 91 | } 92 | 93 | if(YAML > JSON) { 94 | monaco.editor.setModelLanguage(editor.getModel(), "yaml"); 95 | } else { 96 | monaco.editor.setModelLanguage(editor.getModel(), "json"); 97 | } 98 | } 99 | 100 | EventSystem.registerEventCallback("Codestrates.Editor.Opened", (evt)=>{ 101 | let editor = evt.detail.editor; 102 | 103 | if(editor.fragment instanceof ConceptDefinitionFragment) { 104 | editor.fragment.registerOnFragmentChangedHandler(() => { 105 | detectYAMLJSON(editor.editor); 106 | }); 107 | detectYAMLJSON(editor.editor); 108 | } 109 | }); 110 | -------------------------------------------------------------------------------- /codestrates/blockly-editor/SVGIconButtonField.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SVGIconButtonField - SVG icons in blockly 3 | * 4 | * This code is licensed under the MIT License (MIT). 5 | * 6 | * Copyright 2020, 2021, 2022 Rolf Bagge, Janus B. Kristensen, CAVI, 7 | * Center for Advanced Visualization and Interaction, Aarhus University 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the “Software”), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in 17 | * all copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | * THE SOFTWARE. 26 | * 27 | */ 28 | 29 | class SVGIconButtonField extends Blockly.Field { 30 | constructor(iconUrl, width = 16, height = 16) { 31 | super(); 32 | 33 | this.size_ = new Blockly.utils.Size(width, height); 34 | this.CURSOR = "pointer"; 35 | this.isDirty_ = false; 36 | this.SERIALIZABLE = false; 37 | this.EDITABLE = false; 38 | 39 | this.iconUrl = iconUrl; 40 | 41 | this.clickCallbacks = []; 42 | } 43 | 44 | initView() { 45 | let self = this; 46 | 47 | this.imageElement_ = Blockly.utils.dom.createSvgElement( 48 | "use", { 49 | 'height': this.size_.height + 'px', 50 | 'width': this.size_.width + 'px', 51 | 'alt': "" 52 | }, this.fieldGroup_); 53 | this.imageElement_.setAttributeNS(Blockly.utils.dom.XLINK_NS, 'xlink:href', this.iconUrl); 54 | this.imageElement_.classList.add("varvClickableIcon"); 55 | } 56 | 57 | bindEvents_() { 58 | let self = this; 59 | 60 | super.bindEvents_(); 61 | 62 | Blockly.bindEventWithChecks_(this.getClickTarget_(), 'mouseup', this, 63 | function (_event) { 64 | self.triggerCallbacks(); 65 | } 66 | ); 67 | } 68 | 69 | click() { 70 | this.triggerCallbacks(); 71 | } 72 | 73 | triggerCallbacks() { 74 | this.clickCallbacks.forEach((callback) => { 75 | callback(); 76 | }); 77 | } 78 | 79 | registerClickCallback(callback) { 80 | this.clickCallbacks.push(callback); 81 | } 82 | } 83 | 84 | window.SVGIconButtonField = SVGIconButtonField; 85 | -------------------------------------------------------------------------------- /codestrates/blockly-editor/blockly-editor.scss: -------------------------------------------------------------------------------- 1 | .blocklyDropDownDiv { 2 | contain: none !important; 3 | overflow: visible !important; 4 | } 5 | 6 | .blocklyWidgetDiv { 7 | contain: none !important; 8 | overflow: visible !important; 9 | } 10 | 11 | .blocklyTooltipDiv { 12 | contain: none !important; 13 | overflow: visible !important; 14 | } 15 | 16 | .varvClickableIcon { 17 | cursor: pointer !important; 18 | } 19 | -------------------------------------------------------------------------------- /codestrates/blockly-editor/blockly_editor_plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /codestrates/blockly-editor/descriptor.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Visual block-based programming editor for Varv", 3 | "dependencies": [ 4 | "wpm_js_libs #BlocklyCore", 5 | "wpm_js_libs #LiveElement", 6 | "wpm_js_libs #potpack", 7 | "codestrates-repos #editor_core", 8 | "#varv-engine" 9 | ], 10 | "assets":[], 11 | "license": "MIT", 12 | "version": "1" 13 | } 14 | -------------------------------------------------------------------------------- /codestrates/descriptor.json: -------------------------------------------------------------------------------- 1 | { 2 | "friendlyName": "Varv codestrates extension", 3 | "description": "Extend Codestrates with Varv fragments", 4 | "dependencies": [ 5 | "codestrates-repos #fragment_core", 6 | "codestrates-repos #js-eval-engine" 7 | ], 8 | "assets": [], 9 | "license": "MIT", 10 | "version": "0.1", 11 | "changelog": {} 12 | } 13 | -------------------------------------------------------------------------------- /core/Behaviour.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Behaviour - Concept behaviours 3 | * 4 | * This code is licensed under the MIT License (MIT). 5 | * 6 | * Copyright 2020, 2021, 2022 Rolf Bagge, Janus B. Kristensen, CAVI, 7 | * Center for Advanced Visualization and Interaction, Aarhus University 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the “Software”), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in 17 | * all copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | * THE SOFTWARE. 26 | * 27 | */ 28 | 29 | /** 30 | * 31 | */ 32 | class Behaviour { 33 | constructor(name, triggers, actions, concept, overrideActionName=null) { 34 | const self = this; 35 | 36 | this.concept = concept; 37 | 38 | if(triggers == null) { 39 | triggers = []; 40 | } 41 | 42 | if(actions == null) { 43 | actions = []; 44 | } 45 | 46 | if(!Array.isArray(triggers)) { 47 | triggers = [triggers]; 48 | } 49 | 50 | if(!Array.isArray(actions)) { 51 | actions = [actions]; 52 | } 53 | 54 | this.cloneData = { 55 | name: name, 56 | triggers: triggers!=null?JSON.parse(JSON.stringify(triggers)):null, 57 | actions: actions!=null?JSON.parse(JSON.stringify(actions)):null, 58 | overrideActionName: overrideActionName 59 | } 60 | 61 | this.name = name; 62 | 63 | this.triggers = triggers.map((triggerJson)=>{ 64 | if(typeof triggerJson === "string") { 65 | if(!Trigger.isTriggerType(triggerJson)) { 66 | console.log("Unknown trigger, guessing it is an action trigger:", triggerJson); 67 | triggerJson = {"action": triggerJson}; 68 | } 69 | } 70 | 71 | let originalStringTrigger = null; 72 | 73 | if(typeof triggerJson === "string") { 74 | originalStringTrigger = triggerJson; 75 | 76 | triggerJson = {}; 77 | triggerJson[originalStringTrigger] = {}; 78 | } 79 | 80 | if(typeof triggerJson !== "string") { 81 | 82 | let triggerName = UUIDGenerator.generateUUID("trigger-"); 83 | let trigger = ConceptLoader.parseTrigger(triggerName, triggerJson, concept); 84 | 85 | if(trigger != null) { 86 | self.concept.addTrigger(trigger); 87 | 88 | return triggerName; 89 | } else { 90 | if(originalStringTrigger == null) { 91 | console.warn("Unable to parse anonymous trigger:", triggerJson); 92 | } else { 93 | return originalStringTrigger; 94 | } 95 | } 96 | 97 | return null; 98 | } 99 | 100 | return triggerJson; 101 | }).filter((trigger)=>{ 102 | return typeof trigger === "string"; 103 | }); 104 | 105 | this.deleteCallbacks = []; 106 | 107 | let actionName = name+".actions"; 108 | 109 | if(overrideActionName != null) { 110 | actionName = overrideActionName; 111 | this.callableAction = true; 112 | } 113 | 114 | this.actionChain = new ActionChain(actionName, {}, this.concept); 115 | 116 | actions.forEach((actionJson)=>{ 117 | if(typeof actionJson !== "string") { 118 | let actionName = UUIDGenerator.generateUUID("action-"); 119 | 120 | if(!Array.isArray(actionJson)) { 121 | actionJson = [actionJson]; 122 | } 123 | 124 | let action = ConceptLoader.parseAction(actionName, actionJson, self.concept); 125 | 126 | if(action != null) { 127 | self.actionChain.addAction(action); 128 | } else { 129 | console.warn("Unable to parse anonymous action:", actionJson); 130 | } 131 | } else { 132 | let action = null; 133 | 134 | //Check for primitive action first. 135 | if(Action.hasPrimitiveAction(actionJson)) { 136 | action = Action.getPrimitiveAction(actionJson, {}, concept); 137 | } else { 138 | action = new LookupActionAction("", { 139 | lookupActionName: actionJson 140 | }, concept); 141 | } 142 | 143 | self.actionChain.addAction(action); 144 | } 145 | }); 146 | } 147 | 148 | cloneFresh(concept) { 149 | return new Behaviour(this.cloneData.name, this.cloneData.triggers, this.cloneData.actions, concept, this.cloneData.overrideActionName); 150 | } 151 | 152 | setupEvents() { 153 | const self = this; 154 | 155 | this.triggers.forEach((trigger)=>{ 156 | self.deleteCallbacks.push(Trigger.registerTriggerEvent(trigger, async (context) => { 157 | try { 158 | await self.onTrigger(trigger, context); 159 | } catch(e) { 160 | console.error(e); 161 | } 162 | })); 163 | }); 164 | } 165 | 166 | async onTrigger(triggerName, context) { 167 | try { 168 | await ActionTrigger.before(this.actionChain, context); 169 | let mark = VarvPerformance.start(); 170 | let resultContext = await this.actionChain.apply(context); 171 | if(this.actionChain.isPrimitive) { 172 | VarvPerformance.stop("PrimitiveAction-"+this.actionChain.name, mark); 173 | } else { 174 | VarvPerformance.stop("CustomAction-"+this.actionChain.name, mark); 175 | } 176 | await ActionTrigger.after(this.actionChain, resultContext); 177 | } catch(e) { 178 | if(e instanceof StopError) { 179 | //console.log("We stopped the chain: "+e.message); 180 | } else { 181 | throw e; 182 | } 183 | } 184 | } 185 | 186 | destroy() { 187 | const self = this; 188 | 189 | 190 | this.deleteCallbacks.forEach((deleteCallback)=>{ 191 | deleteCallback.delete(); 192 | }); 193 | this.triggers.forEach((triggerName)=>{ 194 | let trigger = self.concept.getTrigger(triggerName); 195 | if(trigger != null) { 196 | self.concept.removeTrigger(trigger); 197 | } 198 | }); 199 | this.deleteCallbacks = null; 200 | this.triggers = null; 201 | this.actionChain = null; 202 | } 203 | } 204 | 205 | window.Behaviour = Behaviour; 206 | -------------------------------------------------------------------------------- /core/Datastore.js: -------------------------------------------------------------------------------- 1 | /** 2 | * DataStore - Superclass for all backing stores 3 | * 4 | * This code is licensed under the MIT License (MIT). 5 | * 6 | * Copyright 2020, 2021, 2022 Rolf Bagge, Janus B. Kristensen, CAVI, 7 | * Center for Advanced Visualization and Interaction, Aarhus University 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the “Software”), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in 17 | * all copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | * THE SOFTWARE. 26 | * 27 | */ 28 | 29 | /** 30 | * Datastores 31 | * @namespace Datastores 32 | */ 33 | 34 | // superclass for all datastores (mostly empty for potential later introspection code) 35 | class Datastore { 36 | constructor(name, options){ 37 | this.name = name; 38 | this.options = options; 39 | this.mappedConcepts = new Map(); 40 | } 41 | 42 | isConceptMapped(concept){ 43 | return this.isConceptTypeMapped(concept.name); 44 | } 45 | isConceptTypeMapped(conceptTypeName){ 46 | return this.mappedConcepts.has(conceptTypeName); 47 | } 48 | isPropertyMapped(concept, property){ 49 | if (!this.isConceptMapped(concept)) return false; 50 | return this.mappedConcepts.get(concept.name).has(property.name); 51 | } 52 | isPropertyNameMapped(conceptName, propertyName){ 53 | if (!this.isConceptTypeMapped(conceptName)) return false; 54 | return this.mappedConcepts.get(conceptName).has(propertyName); 55 | } 56 | internalAddConceptMapping(concept){ 57 | if (!this.isConceptMapped(concept)) 58 | this.mappedConcepts.set(concept.name, new Map()); 59 | } 60 | internalAddPropertyMapping(concept, property, trackingData={}){ 61 | if (this.isPropertyMapped(concept, property)){ 62 | throw new Error('Already has internal mapping for '+concept+"."+property); 63 | } 64 | this.internalAddConceptMapping(concept); 65 | let propertyMap = this.mappedConcepts.get(concept.name); 66 | propertyMap.set(property.name, trackingData); 67 | } 68 | internalRemovePropertyMapping(concept, property){ 69 | if (!this.isConceptMapped(concept)) throw new Error("Concept not mapped when trying to remove "+concept+"."+property); 70 | let propertyMap = this.mappedConcepts.get(concept.name); 71 | propertyMap.delete(property.name); 72 | } 73 | 74 | internalPropertyTrackingData(concept, property){ 75 | return this.mappedConcepts.get(concept.name).get(property.name); 76 | } 77 | 78 | createBackingStore(concept, property) { 79 | throw new Error("createBackingStore, should always be overridden in Datastore subclass - "+this.constructor.name); 80 | } 81 | 82 | removeBackingStore(concept, property) { 83 | throw new Error("removeBackingStore, should always be overridden in Datastore subclass - "+this.constructor.name); 84 | } 85 | 86 | isShared() { 87 | return true; 88 | } 89 | 90 | async init() { 91 | throw new Error("init, should always be overridden in Datastore subclass - "+this.constructor.name); 92 | } 93 | 94 | destroy() { 95 | throw new Error("destroy, should always be overridden in Datastore subclass - "+this.constructor.name); 96 | } 97 | 98 | /** 99 | * 100 | * @param {String[]} typeNames 101 | * @param {Filter} query 102 | * @param {VarvContext} context 103 | * @param {number} limit 104 | * @param {Concept} localConcept 105 | * @returns {Promise} 106 | */ 107 | async lookupInstances(typeNames, query, context, limit = 0, localConcept = null) { 108 | throw new Error("Implement [lookupInstances] me in sub datastores! - "+this.constructor.name); 109 | } 110 | 111 | /** 112 | * @param {String[]} typeNames 113 | * @param {Filter} query 114 | * @param {VarvContext} context 115 | * @param {Concept} localConcept 116 | * @returns {Promise} 117 | */ 118 | async countInstances(typeNames, query, context, localConcept) { 119 | throw new Error("Implement [countInstances] me in sub datastores! - "+this.constructor.name); 120 | } 121 | 122 | /** 123 | * @param {String[]} typeNames 124 | * @param {Filter} query 125 | * @param {VarvContext} context 126 | * @param {Concept} localConcept 127 | * @returns {Promise} 128 | */ 129 | async existsInstance(typeNames, query, context, localConcept) { 130 | throw new Error("Implement [existsInstance] me in sub datastores! - "+this.constructor.name); 131 | } 132 | 133 | /** 134 | * 135 | * @param {String} uuid 136 | * @returns {Promise} 137 | */ 138 | async lookupConcept(uuid) { 139 | throw new Error("Implement [lookupConcept] me in sub datastores! - "+this.constructor.name); 140 | } 141 | 142 | static getDatastoreFromName(name) { 143 | return Datastore.datastores.get(name); 144 | } 145 | 146 | static registerDatastoreType(name, datastoreType) { 147 | Datastore.datastoreTypes.set(name, datastoreType); 148 | } 149 | 150 | static getDatastoreType(name) { 151 | return Datastore.datastoreTypes.get(name); 152 | } 153 | 154 | static getAllDatastores() { 155 | return Array.from(Datastore.datastores.values()); 156 | } 157 | } 158 | Datastore.DEBUG = false; 159 | Datastore.datastores = new Map(); 160 | Datastore.optionalDatastores = ["cauldron"]; // A list of datastores for which there is no warning emitted if not found 161 | Datastore.datastoreTypes = new Map(); 162 | 163 | window.Datastore = Datastore; 164 | -------------------------------------------------------------------------------- /core/DirectDatastore.js: -------------------------------------------------------------------------------- 1 | class DirectDatastore extends Datastore { 2 | constructor(name, options) { 3 | super(name, options); 4 | 5 | this.conceptUUIDMap = new Map(); 6 | this.conceptTypeUUIDMap = new Map(); 7 | } 8 | 9 | registerConceptFromUUID(uuid, concept) { 10 | if(this.isConceptMapped(concept)) { 11 | this.conceptUUIDMap.set(uuid, concept); 12 | 13 | let uuidSet = this.conceptTypeUUIDMap.get(concept.name); 14 | if (uuidSet == null) { 15 | uuidSet = new Set(); 16 | this.conceptTypeUUIDMap.set(concept.name, uuidSet); 17 | } 18 | 19 | uuidSet.add(uuid); 20 | } 21 | } 22 | 23 | deregisterConceptFromType(type) { 24 | const self = this; 25 | 26 | this.getAllUUIDsFromType(type).forEach((uuid)=>{ 27 | self.deregisterConceptFromUUID(uuid); 28 | }); 29 | } 30 | 31 | deregisterConceptFromUUID(uuid) { 32 | let concept = this.getConceptFromUUID(uuid); 33 | this.conceptUUIDMap.delete(uuid); 34 | if(concept != null) { 35 | let uuidSet = this.conceptTypeUUIDMap.get(concept.name); 36 | if (uuidSet != null) { 37 | uuidSet.delete(uuid); 38 | } 39 | } 40 | } 41 | 42 | getConceptFromUUID(uuid) { 43 | return this.conceptUUIDMap.get(uuid); 44 | } 45 | 46 | getAllUUIDsFromType(type, includeOtherConcepts = false) { 47 | const self = this; 48 | 49 | let uuidSet = null; 50 | if(!includeOtherConcepts) { 51 | uuidSet = this.conceptTypeUUIDMap.get(type); 52 | } else { 53 | uuidSet = new Set(); 54 | VarvEngine.getAllImplementingConcepts(type).forEach((concept)=>{ 55 | self.getAllUUIDsFromType(concept.name, false).forEach((uuid)=>{ 56 | uuidSet.add(uuid); 57 | }); 58 | }); 59 | } 60 | if (uuidSet == null) { 61 | return []; 62 | } 63 | return Array.from(uuidSet); 64 | } 65 | 66 | async countInstances(typeNames, query, context, localConcept) { 67 | let uuids = await this.lookupInstances(typeNames, query, context, 0, localConcept); 68 | return uuids.length; 69 | } 70 | 71 | async existsInstance(typeNames, query, context, localConcept) { 72 | let uuids = await this.lookupInstances(typeNames, query, context, 1, localConcept); 73 | return uuids.length > 0; 74 | } 75 | 76 | async lookupInstances(typeNames, query, context, limit, localConcept) { 77 | const self = this; 78 | 79 | let uuidSet = new Set(); 80 | 81 | let markStart = VarvPerformance.start(); 82 | 83 | typeNames.forEach((type)=>{ 84 | self.getAllUUIDsFromType(type, false).forEach((uuid)=>{ 85 | uuidSet.add(uuid); 86 | }); 87 | }); 88 | 89 | VarvPerformance.stop("DirectDatastore.lookupInstances.getAll", markStart); 90 | 91 | let result = []; 92 | 93 | if(query != null) { 94 | let markFilter = VarvPerformance.start(); 95 | let allPromises = []; 96 | for(let uuid of uuidSet) { 97 | allPromises.push(query.filter({target: uuid}, localConcept)); 98 | } 99 | 100 | let filterResult = await Promise.all(allPromises); 101 | 102 | let index = 0; 103 | for(let uuid of uuidSet) { 104 | if (filterResult[index]) { 105 | result.push(uuid); 106 | } 107 | index++; 108 | } 109 | 110 | VarvPerformance.stop("DirectDatastore.lookupInstances.filter", markFilter); 111 | } else { 112 | result.push(...uuidSet); 113 | } 114 | 115 | if(limit > 0 && result.length > limit) { 116 | result.splice(limit, result.length-limit); 117 | } 118 | 119 | VarvPerformance.stop("DirectDatastore.lookupInstances", markStart); 120 | 121 | return result; 122 | } 123 | 124 | async lookupConcept(uuid) { 125 | let result = this.getConceptFromUUID(uuid); 126 | 127 | if(result == null) { 128 | throw Error("Unable to find concept from uuid: "+uuid); 129 | } 130 | 131 | return result; 132 | } 133 | } 134 | window.DirectDatastore = DirectDatastore; 135 | -------------------------------------------------------------------------------- /core/PropertyCache.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | class PropertyCache { 4 | static getCachedProperty(lookupKey) { 5 | if(PropertyCache.cacheEnabled) { 6 | let value = PropertyCache.cacheMap.get(lookupKey); 7 | 8 | if(PropertyCache.DEBUG) { 9 | console.groupCollapsed("PropertyCache getting: ", lookupKey, value); 10 | console.trace(); 11 | console.groupEnd(); 12 | } 13 | 14 | return value; 15 | } else { 16 | return null; 17 | } 18 | } 19 | 20 | static removeCachedProperty(lookupKey) { 21 | if(PropertyCache.DEBUG) { 22 | console.log("PropertyCache deleting: ", lookupKey); 23 | } 24 | PropertyCache.cacheMap.delete(lookupKey); 25 | } 26 | 27 | static setCachedProperty(lookupKey, value) { 28 | if(PropertyCache.DEBUG) { 29 | console.log("PropertyCache setting: ", lookupKey, value); 30 | } 31 | PropertyCache.cacheMap.set(lookupKey, value); 32 | } 33 | 34 | static reset() { 35 | PropertyCache.cacheMap.clear(); 36 | } 37 | } 38 | PropertyCache.DEBUG = false; 39 | PropertyCache.cacheEnabled = true; 40 | PropertyCache.cacheMap = new Map(); 41 | window.PropertyCache = PropertyCache; 42 | -------------------------------------------------------------------------------- /core/Trigger.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Trigger - Superclass for all triggers 3 | * 4 | * This code is licensed under the MIT License (MIT). 5 | * 6 | * Copyright 2020, 2021, 2022 Rolf Bagge, Janus B. Kristensen, CAVI, 7 | * Center for Advanced Visualization and Interaction, Aarhus University 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the “Software”), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in 17 | * all copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | * THE SOFTWARE. 26 | * 27 | */ 28 | 29 | /** 30 | * Triggers are used to do something when something happens 31 | * @namespace Triggers 32 | */ 33 | 34 | const VarvEventPrefix = "VarvEvent."; 35 | let triggeringEnabled = true; 36 | 37 | class Trigger { 38 | /** 39 | * Create a new trigger 40 | * @param {string} name - The name of this trigger 41 | * @param {object|string} options - The options of this trigger 42 | * @param {Concept} concept - The owning concept of this trigger 43 | */ 44 | constructor(name, options, concept) { 45 | this.options = options; 46 | this.name = name; 47 | this.concept = concept; 48 | } 49 | 50 | /** 51 | * Destroy this trigger 52 | * @param {Concept} concept - The concept this trigger is registered on 53 | */ 54 | destroy() { 55 | //Default implementation just disabled the trigger 56 | this.disable(); 57 | } 58 | 59 | /** 60 | * Enable this trigger 61 | */ 62 | enable() { 63 | console.warn("Always override Trigger.enable in subclass!"); 64 | } 65 | 66 | /** 67 | * Disable this trigger 68 | */ 69 | disable() { 70 | console.warn("Always override Trigger.disable in subclass!"); 71 | } 72 | 73 | /** 74 | * Register a new type of trigger 75 | * @param {string} type - The type of trigger to register 76 | * @param {Trigger} trigger - The trigger to register 77 | */ 78 | static registerTrigger(type, trigger) { 79 | Trigger.triggers.set(type, trigger); 80 | } 81 | 82 | /** 83 | * Checks if the given type corresponds to a trigger type 84 | * @param {tring} type the type to check 85 | * @returns {boolean} true/false depending on if type was a trigger type 86 | */ 87 | static isTriggerType(type) { 88 | return Trigger.triggers.has(type); 89 | } 90 | 91 | /** 92 | * Create a new named instance of the given trigger type, using the given options 93 | * @param {string} type - The type of trigger to create 94 | * @param {string} name - The name to give the trigger 95 | * @param {object} options - The options to pass along to the trigger 96 | * @returns {Trigger} - The newly created trigger 97 | */ 98 | static getTrigger(type, name, options, concept) { 99 | let triggerClass = Trigger.triggers.get(type); 100 | 101 | if (triggerClass == null) { 102 | throw new Error("Unknown trigger [" + type + "]"); 103 | } 104 | 105 | return new triggerClass(name, options, concept); 106 | } 107 | 108 | static registerTriggerEvent(triggerName, callback) { 109 | return EventSystem.registerEventCallback(VarvEventPrefix+triggerName, async (evt)=>{ 110 | let contexts = Action.clone(evt.detail); 111 | 112 | let callbackReturn = callback(contexts); 113 | 114 | if(callbackReturn instanceof Promise) { 115 | await callbackReturn; 116 | } 117 | }); 118 | } 119 | 120 | static async trigger(triggerName, context) { 121 | const mark = VarvPerformance.start(); 122 | if(!triggeringEnabled) { 123 | if(Trigger.DEBUG) { 124 | console.log("Skipping (Triggering Disabled):", triggerName, context); 125 | } 126 | return; 127 | } 128 | 129 | if(!Array.isArray(context)) { 130 | context = [context]; 131 | } 132 | 133 | if(Trigger.DEBUG) { 134 | console.group("Triggering:", triggerName, Action.clone(context)); 135 | } 136 | 137 | try { 138 | await EventSystem.triggerEventAsync(VarvEventPrefix + triggerName, context); 139 | VarvPerformance.stop("Trigger-"+triggerName, mark); 140 | } catch(e) { 141 | VarvPerformance.stop("Trigger-"+triggerName, mark); 142 | throw e; 143 | } 144 | 145 | if(Trigger.DEBUG) { 146 | console.groupEnd(); 147 | } 148 | } 149 | 150 | static async runWithoutTriggers(method) { 151 | triggeringEnabled = false; 152 | await method(); 153 | triggeringEnabled = true; 154 | } 155 | } 156 | Trigger.DEBUG = false; 157 | Trigger.triggers = new Map(); 158 | window.Trigger = Trigger; 159 | -------------------------------------------------------------------------------- /core/VarvPerformance.js: -------------------------------------------------------------------------------- 1 | class VarvPerformance { 2 | static makeId(length) { 3 | let result = ''; 4 | const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 5 | const charactersLength = characters.length; 6 | for ( let i = 0; i < length; i++ ) { 7 | result += characters.charAt(Math.floor(Math.random() * 8 | charactersLength)); 9 | } 10 | return result; 11 | } 12 | 13 | static start() { 14 | if(!VarvPerformance.enabled) { 15 | return; 16 | } 17 | 18 | let mark = this.makeId(32); 19 | 20 | performance.mark(mark); 21 | 22 | return mark; 23 | } 24 | 25 | static stop(name, mark, details) { 26 | if(!VarvPerformance.enabled) { 27 | return; 28 | } 29 | 30 | let endMark = mark+"end"; 31 | 32 | performance.mark(endMark); 33 | 34 | if(VarvPerformance.prefix.length > 0) { 35 | name = VarvPerformance.prefix+"."+name; 36 | } 37 | 38 | try { 39 | performance.measure(name, { 40 | start: mark, 41 | end: endMark, 42 | detail: JSON.stringify(details, (key, value) => { 43 | if (typeof value === "object" && value?.hasOwnProperty("constructOptions")) { 44 | return value.constructOptions; 45 | } 46 | 47 | return value; 48 | }) 49 | }); 50 | } catch(e) { 51 | performance.measure(name, mark, endMark); 52 | } 53 | } 54 | 55 | static showInternal(options, callback) { 56 | let defaultOptions = { 57 | regex: null, 58 | minInvocations: 0 59 | }; 60 | 61 | options = Object.assign({}, defaultOptions, options); 62 | 63 | let measureMap = new Map(); 64 | 65 | performance.getEntriesByType("measure").forEach((measure)=>{ 66 | let list = measureMap.get(measure.name); 67 | if(list == null) { 68 | list = []; 69 | measureMap.set(measure.name, list); 70 | } 71 | 72 | list.push(measure); 73 | }); 74 | 75 | let entryArray = Array.from(measureMap.entries()); 76 | 77 | entryArray.sort((e1, e2)=>{ 78 | let v1 = e1[1]; 79 | let v2 = e2[1]; 80 | 81 | return v2.length - v1.length; 82 | }); 83 | 84 | entryArray.forEach((entry)=>{ 85 | let measures = entry[1]; 86 | let key = entry[0]; 87 | 88 | if(measures.length < options.minInvocations) { 89 | return; 90 | } 91 | 92 | if(options.regex != null) { 93 | if(key.match(options.regex) == null) { 94 | return; 95 | } 96 | } 97 | 98 | measures.sort((m1, m2)=>{ 99 | return m2.duration - m1.duration; 100 | }); 101 | 102 | callback(measures, key); 103 | }); 104 | } 105 | 106 | static showDetails(options) { 107 | this.showInternal(options, (measures, key)=>{ 108 | 109 | console.groupCollapsed(key+" x"+measures.length); 110 | 111 | measures.forEach((measure)=>{ 112 | if(measure.detail != null) { 113 | console.log(measure.duration.toFixed(3) + " ms - ", JSON.parse(measure.detail)); 114 | } else { 115 | console.log(measure.duration.toFixed(3) + " ms"); 116 | } 117 | }); 118 | 119 | console.groupEnd(); 120 | }); 121 | } 122 | 123 | static showStats(options) { 124 | let data = []; 125 | 126 | this.showInternal(options, (measures, key)=> { 127 | let measureDurations = measures.map((m)=>{ 128 | return m.duration; 129 | }); 130 | 131 | let min = 99999; 132 | let max = 0; 133 | let mean = 0; 134 | let median = -1; 135 | let sum = 0; 136 | 137 | measureDurations.forEach((d)=>{ 138 | min = Math.min(min, d); 139 | max = Math.max(max, d); 140 | sum += d; 141 | }); 142 | 143 | mean = sum / measureDurations.length; 144 | 145 | let middleIndex = Math.floor(measureDurations.length / 2); 146 | 147 | if(measureDurations.length % 2 === 1) { 148 | //Odd 149 | median = measureDurations[middleIndex]; 150 | } else { 151 | //Even 152 | median = measureDurations[middleIndex-1]/2.0 + measureDurations[middleIndex] / 2.0; 153 | } 154 | 155 | data.push({ 156 | "name": key, 157 | "invocations": measureDurations.length, 158 | "mean": +mean.toFixed(3), 159 | "min": +min.toFixed(3), 160 | "max": +max.toFixed(3), 161 | "median": +median.toFixed(3), 162 | "sum": +sum.toFixed(3) 163 | }) 164 | }); 165 | 166 | console.table(data); 167 | } 168 | 169 | static reset() { 170 | performance.clearMarks(); 171 | performance.clearMeasures(); 172 | } 173 | } 174 | 175 | VarvPerformance.enabled = false; 176 | VarvPerformance.prefix = ""; 177 | window.VarvPerformance = VarvPerformance; 178 | -------------------------------------------------------------------------------- /core/YAMLJSONConverter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * YAMLJSONConverter - Convert between JSON and YAML program code 3 | * 4 | * This code is licensed under the MIT License (MIT). 5 | * 6 | * Copyright 2020, 2021, 2022 Rolf Bagge, Janus B. Kristensen, CAVI, 7 | * Center for Advanced Visualization and Interaction, Aarhus University 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the “Software”), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in 17 | * all copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | * THE SOFTWARE. 26 | * 27 | */ 28 | 29 | /** 30 | * 31 | */ 32 | class YAMLJSONConverter { 33 | /** 34 | * True/False depending on if the string code can be parsed as YAML 35 | * @param {string} code 36 | */ 37 | static isYAML(code) { 38 | try { 39 | jsyaml.load(code); 40 | return true; 41 | } catch(e) { 42 | //Ignore 43 | } 44 | 45 | return false; 46 | } 47 | 48 | /** 49 | * True/False depending on if the string code can be parsed as JSON 50 | * @param {string} code 51 | */ 52 | static isJSON(code) { 53 | try { 54 | JSON.parse(code); 55 | return true; 56 | } catch(e) { 57 | //Ignore 58 | } 59 | 60 | return false; 61 | } 62 | 63 | /** 64 | * Parses the given code string into an object, using either YAML or JSON 65 | * @param {string} code 66 | */ 67 | static loadFromString(code) { 68 | let obj = null; 69 | let loader = null; 70 | 71 | try { 72 | obj = JSON.parse(code); 73 | loader = "JSON"; 74 | } catch(jsonEx) { 75 | try { 76 | obj = jsyaml.load(code); 77 | loader = "YAML"; 78 | } catch(yamlEx) { 79 | throw new Error("Unable to Parse string as YAML ("+yamlEx+") or JSON ("+jsonEx+")"); 80 | } 81 | } 82 | 83 | return { 84 | loader, 85 | obj 86 | }; 87 | } 88 | 89 | /** 90 | * Converts the given code to YAML / JSON, to opposite of what the input was. 91 | * @param code 92 | */ 93 | static convert(code) { 94 | let result = YAMLJSONConverter.loadFromString(code); 95 | 96 | switch(result.loader) { 97 | case "JSON": 98 | return jsyaml.dump(result.obj); 99 | break; 100 | case "YAML": 101 | return JSON.stringify(result.obj, null, 2); 102 | break; 103 | default: 104 | console.warn("Unknown obj loader..."); 105 | return code; 106 | } 107 | } 108 | } 109 | window.YAMLJSONConverter = YAMLJSONConverter; 110 | -------------------------------------------------------------------------------- /core/descriptor.json: -------------------------------------------------------------------------------- 1 | { 2 | "friendlyName": "Varv engine", 3 | "description": "The core of the Varv system", 4 | "dependencies": [ 5 | "wpm_js_libs #UUIDGenerator", 6 | "wpm_js_libs #js-yaml", 7 | "wpm_js_libs #LiveElement", 8 | "wpm_js_libs #izitoast", 9 | "wpm_js_libs #Observer", 10 | "codestrates-repos #EventSystem" 11 | ], 12 | "license": "MIT", 13 | "assets": [], 14 | "version": "0.1", 15 | "changelog": {} 16 | } 17 | -------------------------------------------------------------------------------- /datastores/dom/descriptor.json: -------------------------------------------------------------------------------- 1 | { 2 | "friendlyName": "Varv DOM Data Store", 3 | "description": "Allow data to be stored in the browser DOM", 4 | "dependencies": [ 5 | "#varv-engine" 6 | ], 7 | "assets": [], 8 | "license": "MIT", 9 | "version": "0.1", 10 | "changelog": {} 11 | } 12 | -------------------------------------------------------------------------------- /datastores/giga/descriptor.json: -------------------------------------------------------------------------------- 1 | { 2 | "friendlyName": "GigaVarv Websocket backend connector", 3 | "description": "Allows datastores to talk to common database backend systems over a websocket", 4 | "dependencies": [ 5 | "#varv-engine" 6 | ], 7 | "assets": [], 8 | "license": "MIT", 9 | "version": "0.1", 10 | "changelog": {} 11 | } 12 | -------------------------------------------------------------------------------- /datastores/localstorage/descriptor.json: -------------------------------------------------------------------------------- 1 | { 2 | "friendlyName": "Varv localStorage Datastore", 3 | "description": "Store data in the browsers's local storage database", 4 | "dependencies": [ 5 | "#varv-engine" 6 | ], 7 | "assets": [], 8 | "license": "MIT", 9 | "version": "0.1", 10 | "changelog": {} 11 | } 12 | -------------------------------------------------------------------------------- /datastores/location/LocationDataStore.js: -------------------------------------------------------------------------------- 1 | /** 2 | * LocationDataStore - access/control the URL in browsers 3 | * 4 | * This code is licensed under the MIT License (MIT). 5 | * 6 | * Copyright 2020, 2021, 2022 Rolf Bagge, Janus B. Kristensen, CAVI, 7 | * Center for Advanced Visualization and Interaction, Aarhus University 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the “Software”), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in 17 | * all copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | * THE SOFTWARE. 26 | * 27 | */ 28 | 29 | /** 30 | * A datastore that allows reading and writing the location, query and hash as data. 31 | * 32 | * This datastore registers as the type "location". 33 | * 34 | * Any field mapped to this datastore from any object will use the URL params as the 35 | * source of the data. For example mapping the field "useBirds" will allow reading the 36 | * xyz value of a ?fun=true&useBirds=xyz URL. 37 | * 38 | * The special mapping "locationHash" allows reading the hash-postfix of the URL. For 39 | * example the "cats" from https://cavi.au.dk/#cats 40 | * 41 | * Listening for changes to the mapped locationHash will notify the program on load 42 | * as well as if/when the hash changes. 43 | * 44 | * This datastore has no options. 45 | * 46 | * @memberOf Datastores 47 | */ 48 | class LocationDataStore extends DirectDatastore { 49 | constructor(name, options = {}) { 50 | super(name, options); 51 | 52 | this.deleteCallbacks = []; 53 | } 54 | 55 | isShared() { 56 | return false; 57 | } 58 | 59 | destroy() { 60 | this.deleteCallbacks.forEach((deleteCallback)=>{ 61 | deleteCallback.delete(); 62 | }); 63 | } 64 | 65 | async init() { 66 | const self = this; 67 | 68 | this.deleteCallbacks.push(VarvEngine.registerEventCallback("disappeared", async (context)=> { 69 | if(LocationDataStore.DEBUG) { 70 | console.log("Saw disappeared UUID (LocationDataStore):", context.target); 71 | } 72 | // Do nothing? 73 | })); 74 | this.deleteCallbacks.push(VarvEngine.registerEventCallback("appeared", async (context)=> { 75 | if(LocationDataStore.DEBUG) { 76 | console.log("Saw appeared UUID (LocationDataStore):", context.target); 77 | } 78 | let mark = VarvPerformance.start(); 79 | // Do nothing? Maybe call them like onload? 80 | VarvPerformance.stop("LocationDataStore.registerEventCallback.appeared", mark); 81 | })); 82 | 83 | window.addEventListener("hashchange", function locationHashChanged(){ 84 | self.onHashChange(); 85 | }); 86 | } 87 | 88 | createBackingStore(concept, property) { 89 | const self = this; 90 | if (this.isPropertyMapped(concept,property)) return; 91 | 92 | let setter = (uuid, value) => { 93 | let mark = VarvPerformance.start(); 94 | 95 | if (property.name==="locationHash"){ 96 | if (decodeURIComponent(location.hash.substring(1))!==value){ 97 | location.hash = value; 98 | } 99 | } else { 100 | // This is a parameter/location change if it differs from current 101 | let urlParams = new URLSearchParams(window.location.search); 102 | if (urlParams.has(property.name)){ 103 | if (urlParams.get(property.name)!=value){ 104 | if (LocationDataStore.DEBUG) console.info("FIXME: Location datastore only supports getting, not setting URL paramters so far"); 105 | } 106 | } 107 | return; 108 | } 109 | 110 | VarvPerformance.stop("LocationDataStore.setter", mark); 111 | }; 112 | let getter = (uuid) => { 113 | let mark = VarvPerformance.start(); 114 | if (property.name==="locationHash"){ 115 | let result = decodeURIComponent(location.hash.substring(1)); 116 | if (result === "") throw new Exception("Cannot get empty locationHash"); 117 | VarvPerformance.stop("LocationDataStore.getter.hash", mark); 118 | return result; 119 | } else { 120 | let urlParams = new URLSearchParams(window.location.search); 121 | if (!urlParams.has(property.name)) throw new Exception("Cannot get property not present in URL"); 122 | VarvPerformance.stop("LocationDataStore.getter.argument", mark); 123 | return urlParams.get(property.name); 124 | } 125 | }; 126 | property.addSetCallback(setter); 127 | property.addGetCallback(getter); 128 | 129 | // Check if concept already is mapped, if not, register it 130 | this.internalAddPropertyMapping(concept, property, {setter: setter, getter: getter}); 131 | } 132 | 133 | removeBackingStore(concept, property) { 134 | if (!this.isPropertyMapped(concept, property)){ 135 | throw new Error('Cannot unmap property from memory because the property was not mapped: ' + concept + "." + property); 136 | } 137 | 138 | let trackingData = this.internalPropertyTrackingData(concept, property); 139 | property.removeSetCallback(trackingData.setter); 140 | property.removeGetCallback(trackingData.getter); 141 | 142 | this.internalRemovePropertyMapping(concept, property); 143 | } 144 | 145 | async onHashChange(){ 146 | if (LocationDataStore.DEBUG) console.log("Location hash changed", location.hash); 147 | // For each of our mapped concepts 148 | for (let [conceptName, properties] of this.mappedConcepts.entries()){ 149 | if (properties.has("locationHash")){ 150 | let concept = VarvEngine.getConceptFromType(conceptName); 151 | let property = concept.getProperty("locationHash"); 152 | let value = decodeURIComponent(location.hash.substring(1)); 153 | 154 | if (LocationDataStore.DEBUG) console.log("Firing location hash property set", property, value); 155 | let uuids = await VarvEngine.getAllUUIDsFromType(concept.name, true); 156 | uuids.forEach(async uuid=>{ 157 | await property.setValue(uuid, value); 158 | }); 159 | } 160 | }; 161 | } 162 | 163 | async loadBackingStore() { 164 | const self = this; 165 | 166 | if (LocationDataStore.DEBUG) console.info("LocationDataStore location is "+location); 167 | 168 | setTimeout(async function onLoadLocationTriggers(){ 169 | if (location.hash){ 170 | await self.onHashChange(); 171 | } 172 | 173 | // On-load set the URL parameters too 174 | for (let [name,value] of new URLSearchParams(window.location.search).entries()){ 175 | for (let [conceptName, properties] of self.mappedConcepts.entries()){ 176 | if (properties.has(name)){ 177 | let concept = VarvEngine.getConceptFromType(conceptName); 178 | let property = concept.getProperty(name); 179 | 180 | if (LocationDataStore.DEBUG) console.log("Firing URL parameter property set", conceptName, concept, property, value); 181 | let uuids = await VarvEngine.getAllUUIDsFromType(conceptName, false); // nulllointer if true? 182 | uuids.forEach(async uuid=>{ 183 | await property.setValue(uuid, value); 184 | }); 185 | } 186 | }; 187 | } 188 | 189 | self.hasLoaded = true; 190 | },0); 191 | } 192 | } 193 | LocationDataStore.DEBUG = false; 194 | window.LocationDataStore = LocationDataStore; 195 | 196 | // Register default dom datastore 197 | Datastore.registerDatastoreType("location", LocationDataStore); 198 | -------------------------------------------------------------------------------- /datastores/location/descriptor.json: -------------------------------------------------------------------------------- 1 | { 2 | "friendlyName": "Varv URL Data Store", 3 | "description": "Access properties of the URL as concept data", 4 | "dependencies": [ 5 | "#varv-engine" 6 | ], 7 | "assets": [], 8 | "license": "MIT", 9 | "version": "0.1", 10 | "changelog": {} 11 | } 12 | -------------------------------------------------------------------------------- /datastores/memory/MemoryDataStore.js: -------------------------------------------------------------------------------- 1 | /** 2 | * MemoryDataStore - stores properties temporarily in memory 3 | * 4 | * This code is licensed under the MIT License (MIT). 5 | * 6 | * Copyright 2020, 2021, 2022 Rolf Bagge, Janus B. Kristensen, CAVI, 7 | * Center for Advanced Visualization and Interaction, Aarhus University 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the “Software”), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in 17 | * all copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | * THE SOFTWARE. 26 | * 27 | */ 28 | 29 | /** 30 | * A general purpose datastore that stores properties temporarily in memory. The 31 | * memory is lost on reload. 32 | * 33 | * This datastore registers as the type "memory". 34 | * 35 | * ### Options 36 | * * "storageName" - The memory bucket name of the storage (default: "memory"). Multiple independant buckets can be maintained 37 | * 38 | * @memberOf Datastores 39 | * @example 40 | * { 41 | * "dataStores": { 42 | * "myDataStore": { 43 | * "type": "memory", 44 | * "options": { 45 | * "storageName": "myMemory" 46 | * } 47 | * }, 48 | * ... 49 | * }, 50 | * ... 51 | * 52 | */ 53 | class MemoryDataStore extends DirectDatastore { 54 | constructor(name, options = {}) { 55 | super(name, options); 56 | 57 | this.deleteCallbacks = []; 58 | } 59 | 60 | isShared() { 61 | return false; 62 | } 63 | 64 | destroy() { 65 | this.deleteCallbacks.forEach((deleteCallback)=>{ 66 | deleteCallback.delete(); 67 | }) 68 | } 69 | 70 | async init() { 71 | const self = this; 72 | this.typeVariable = "__memoryDataStore_internalType"; 73 | 74 | this.storageName = "memory"; 75 | if (this.options.storageName) this.storageName = this.options.storageName; 76 | 77 | this.deleteCallbacks.push(VarvEngine.registerEventCallback("disappeared", async (context)=> { 78 | if(MemoryDataStore.DEBUG) { 79 | console.log("Saw disappeared UUID (MemoryDataStore):", context.target); 80 | } 81 | 82 | context.concept.properties.forEach((property) => { 83 | if (self.isPropertyMapped(context.concept, property)) { 84 | let data = self.internalPropertyTrackingData(context.concept, property); 85 | delete data[context.target]; 86 | } 87 | }); 88 | self.getStorage().delete(context.target); 89 | })); 90 | this.deleteCallbacks.push(VarvEngine.registerEventCallback("appeared", async (context)=> { 91 | if(MemoryDataStore.DEBUG) { 92 | console.log("Saw appeared UUID (MemoryDataStore):", context.target); 93 | } 94 | let mark = VarvPerformance.start(); 95 | if (self.isConceptMapped(context.concept) && !self.getStorage().has(context.target)){ 96 | self.getStorage().set(context.target, { 97 | [self.typeVariable]: context.concept.name 98 | }); 99 | } 100 | VarvPerformance.stop("MemoryDataStore.registerEventCallback.appeared", mark); 101 | })); 102 | } 103 | 104 | getStorage(){ 105 | if (!MemoryDataStore.storages[this.storageName]){ 106 | MemoryDataStore.storages[this.storageName] = new Map(); 107 | } 108 | return MemoryDataStore.storages[this.storageName]; 109 | } 110 | 111 | createBackingStore(concept, property) { 112 | const self = this; 113 | 114 | if (this.isPropertyMapped(concept,property)) return; 115 | 116 | let setter = (uuid, value) => { 117 | let mark = VarvPerformance.start(); 118 | if (!self.getStorage().has(uuid)){ 119 | throw new Error("Tried to set concept in memory that never appeared: "+concept.name+"."+property.name); 120 | } 121 | 122 | let data = self.getStorage().get(uuid); 123 | data[property.name] = value; 124 | VarvPerformance.stop("MemoryDataStore.setter", mark); 125 | }; 126 | let getter = (uuid) => { 127 | let mark = VarvPerformance.start(); 128 | let data = self.getStorage().get(uuid); 129 | if (!data) throw new Error("Tried to get concept from memory that was never set: "+concept.name+"."+property.name); 130 | if (!data.hasOwnProperty(property.name)) throw new Error("Tried to get property from memory that was never set: "+concept.name+"."+property.name); 131 | let result = data[property.name]; 132 | VarvPerformance.stop("MemoryDataStore.getter", mark); 133 | return result; 134 | }; 135 | property.addSetCallback(setter); 136 | property.addGetCallback(getter); 137 | 138 | // Check if concept already is mapped, if not, register it 139 | this.internalAddPropertyMapping(concept, property, {setter: setter, getter: getter}); 140 | } 141 | 142 | removeBackingStore(concept, property) { 143 | if (!this.isPropertyMapped(concept, property)){ 144 | throw new Error('Cannot unmap property from memory because the property was not mapped: ' + concept + "." + property); 145 | } 146 | 147 | let trackingData = this.internalPropertyTrackingData(concept, property); 148 | property.removeSetCallback(trackingData.setter); 149 | property.removeGetCallback(trackingData.getter); 150 | 151 | this.internalRemovePropertyMapping(concept, property); 152 | } 153 | 154 | async loadBackingStore() { 155 | // For each of our stored and mapped concepts 156 | for(let [uuid,storedConcept] of this.getStorage().entries()) { 157 | if (MemoryDataStore.DEBUG) console.log("Loading from memory",uuid,storedConcept); 158 | 159 | let type = storedConcept[this.typeVariable]; 160 | if ((!type) || !this.isConceptTypeMapped(type)){ 161 | if (MemoryDataStore.DEBUG) console.log("Ignoring concept from memory since it is not mapped", storedConcept); 162 | continue; 163 | } 164 | 165 | // Check if already registered and only generate an appear event if not 166 | let conceptByUUID = await VarvEngine.getConceptFromUUID(uuid); 167 | let concept = VarvEngine.getConceptFromType(type); 168 | 169 | this.registerConceptFromUUID(uuid, concept); 170 | 171 | // Stil set the properties that we know about 172 | for (const [propertyName,value] of Object.entries(storedConcept)){ 173 | if (propertyName !== this.typeVariable){ 174 | try { 175 | let property = concept.getProperty(propertyName); 176 | if (MemoryDataStore.DEBUG) console.log("Loading property", property, value); 177 | if (this.isPropertyMapped(concept, property)){ 178 | await property.setValue(uuid, value); 179 | } 180 | } catch (ex){ 181 | console.error("Failed to push concept property from memory to concept", ex); 182 | } 183 | } 184 | } 185 | 186 | if (!conceptByUUID) { 187 | await concept.appeared(uuid); 188 | } 189 | } 190 | } 191 | } 192 | MemoryDataStore.DEBUG = false; 193 | window.MemoryDataStore = MemoryDataStore; 194 | MemoryDataStore.storages = new Map(); 195 | 196 | // Register default dom datastore 197 | Datastore.registerDatastoreType("memory", MemoryDataStore); 198 | -------------------------------------------------------------------------------- /datastores/memory/descriptor.json: -------------------------------------------------------------------------------- 1 | { 2 | "friendlyName": "Varv Memory Data Store", 3 | "description": "Allow data to be stored in memory", 4 | "dependencies": [ 5 | "#varv-engine" 6 | ], 7 | "assets": [], 8 | "license": "MIT", 9 | "version": "0.1", 10 | "changelog": {} 11 | } 12 | -------------------------------------------------------------------------------- /datastores/signaling/descriptor.json: -------------------------------------------------------------------------------- 1 | { 2 | "friendlyName": "Varv Signaling Data Store", 3 | "description": "Live data sharing over webstrate signals", 4 | "dependencies": [ 5 | "#varv-engine" 6 | ], 7 | "assets": [], 8 | "license": "MIT", 9 | "version": "0.1", 10 | "changelog": {} 11 | } 12 | -------------------------------------------------------------------------------- /datastores/sql/SQLDataStore.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SQLDataStore - stores properties in an SQL database through websockets 3 | * 4 | * This code is licensed under the MIT License (MIT). 5 | * 6 | * Copyright 2020, 2021, 2022 Rolf Bagge, Janus B. Kristensen, CAVI, 7 | * Center for Advanced Visualization and Interaction, Aarhus University 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the “Software”), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in 17 | * all copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | * THE SOFTWARE. 26 | * 27 | */ 28 | 29 | /** 30 | * A general purpose datastore that uses an SQL server as backend. 31 | * 32 | * This datastore registers as the type "sql". 33 | * 34 | * | Feature | Remark | 35 | * |---------------------|-----------------------------------------------| 36 | * | Property Lengths | Max 64KB string length | 37 | * | Concept Instances | Max 4294967295 total (including deleted ones) | 38 | * | Properties | No specific limit on number of named properties per concept as long as their names are shorter than 62 chars | 39 | * 40 | * ### Performance 41 | * * Searching for strings that "equals"/"startWith" is really fast 42 | * * Good at combining multiple search requirements 43 | * * Creating instances and setting properties is fairly fast if no reads are performed in-between 44 | * 45 | * ### Options 46 | * * "serverURL" - The URL to the server on the form "ws(s)://yourserver/sql". Note that secure websockets (wss) may be required on some sites. 47 | * * "storageName" - The name of the data bucket you intend to use 48 | * 49 | * @memberOf Datastores 50 | * @example 51 | * Adding the datastore 52 | * { 53 | * "dataStoreds": { 54 | * "myDataStore": { 55 | * "type": "sql", 56 | * "options": { 57 | * "serverURL":"wss://someserver.com/sql", 58 | * "storageName": "mybucketname" 59 | * } 60 | * }, 61 | * ... 62 | * }, 63 | * ... 64 | * 65 | */ 66 | class SQLDataStore extends GigaVarvDatastore { 67 | constructor(name, options = {}) { 68 | super(name, options); 69 | } 70 | 71 | async constructQuery(query, primaryResultConceptName, context, localConcept, queryModifiers){ 72 | // Construct an optimized SQL query based on the Varv query given 73 | let topFilterClass = SQLDataStore.getFilterClass(query); 74 | if (!topFilterClass) throw new Error("SQLDatastore: FIXME: Unsupported or missing query filter type in this datastore", query); 75 | let topFilter = new topFilterClass(query); 76 | 77 | try { 78 | let sensitivityList = []; 79 | let filtering = await topFilter.getSQLQuery(primaryResultConceptName, context, localConcept, sensitivityList); 80 | sensitivityList = [...new Set(sensitivityList)]; // Optimize by making fields unique 81 | let joins = sensitivityList.map((field)=>"JOIN `"+field+"` ON `"+field+"`.id = `"+SQLDataStore.INSTANCE_TABLE+"`.id").join(" "); 82 | 83 | let target = "SELECT uuid"; 84 | switch (queryModifiers.mode){ 85 | case "count": 86 | target = "SELECT COUNT(*)"; 87 | break; 88 | } 89 | if (queryModifiers.offset){ 90 | filtering += " OFFSET "+queryModifiers.offset; 91 | } 92 | if (queryModifiers.limit){ 93 | filtering += " LIMIT "+queryModifiers.limit; 94 | } 95 | 96 | let finalQuery = target+" FROM `"+SQLDataStore.INSTANCE_TABLE+"` "+joins+" WHERE "+filtering; 97 | if (SQLDataStore.DEBUG) console.info(finalQuery); 98 | return finalQuery; 99 | } catch (ex){ 100 | console.warn("Failed to prepare SQL query statement string, abstract and SQL query trees were:", query, topFilter); 101 | throw ex; 102 | } 103 | } 104 | 105 | static getFilterClass(query){ 106 | let topFilterName = "SQL"+(query.constructor.name); 107 | let topFilterClass = SQLFilter.registeredFilters[topFilterName]; 108 | if (typeof topFilterClass === "undefined"){ 109 | return null; 110 | } 111 | return topFilterClass; 112 | } 113 | 114 | async lookupConcept(uuid){ 115 | // STUB: misuse q op for a custom query 116 | if (uuid===null || uuid==="") return null; 117 | let type = await this.request({ 118 | "op": "q", 119 | "q":"SELECT type FROM `"+SQLDataStore.INSTANCE_TABLE+"` WHERE uuid = \""+uuid+"\"" // escape uuid 120 | }); 121 | 122 | if(type.a.length === 0) { 123 | throw Error("Unable to find concept from uuid: "+uuid); 124 | } 125 | 126 | return VarvEngine.getConceptFromType(type.a[0]); 127 | } 128 | } 129 | SQLDataStore.DEBUG = false; 130 | SQLDataStore.INSTANCE_TABLE = "instances"; 131 | window.SQLDataStore = SQLDataStore; 132 | SQLDataStore.storages = new Map(); 133 | 134 | // Register default dom datastore 135 | Datastore.registerDatastoreType("sql", SQLDataStore); 136 | -------------------------------------------------------------------------------- /datastores/sql/descriptor.json: -------------------------------------------------------------------------------- 1 | { 2 | "friendlyName": "Varv SQL Data Store", 3 | "description": "SQL-database datastore", 4 | "dependencies": [ 5 | "#varv-giga" 6 | ], 7 | "assets": [], 8 | "license": "MIT", 9 | "version": "0.1", 10 | "changelog": {} 11 | } 12 | -------------------------------------------------------------------------------- /datastores/wsdata/descriptor.json: -------------------------------------------------------------------------------- 1 | { 2 | "friendlyName": "MyWebstrates Data Object Data Store", 3 | "description": "Live data sharing using the document data object", 4 | "dependencies": [ 5 | "#varv-engine" 6 | ], 7 | "assets": [], 8 | "license": "MIT", 9 | "version": "0.1", 10 | "changelog": {} 11 | } 12 | -------------------------------------------------------------------------------- /descriptor.json: -------------------------------------------------------------------------------- 1 | { 2 | "friendlyName": "Varv", 3 | "description": "The standard meta package for using Varv", 4 | "dependencies": [ 5 | "#varv-engine", 6 | "#varv-builtin-actions", 7 | "#varv-builtin-triggers", 8 | "#varv-dom", 9 | "#varv-domdiff-view", 10 | "#varv-dom-triggers", 11 | "#varv-memory", 12 | "#varv-localstorage", 13 | "#varv-inspector", 14 | "#varv-cauldron-delayloader", 15 | "#varv-codestrates-extensions" 16 | ], 17 | "optionalDependencies": [ 18 | "#varv-cauldron", 19 | "#varv-blockly", 20 | "#varv-inspector" 21 | ], 22 | "assets": [], 23 | "license": "MIT", 24 | "version": "1.0", 25 | "changelog": {} 26 | } 27 | -------------------------------------------------------------------------------- /integration/audio-router/AudioRouterFragment.js: -------------------------------------------------------------------------------- 1 | class AudioRouterFragment extends Fragment { 2 | constructor(html) { 3 | super(html); 4 | 5 | //Setup raw on start 6 | this.oldRaw = this.raw; 7 | } 8 | 9 | async require(options = {}) { 10 | let defaultOptions = { 11 | pretty: false, 12 | auto: true 13 | } 14 | 15 | options = Object.assign({}, defaultOptions, options); 16 | 17 | let fragment = Fragment.create(ConceptDefinitionFragment.type()); 18 | 19 | let json = null; 20 | 21 | try { 22 | json = JSON.parse(this.raw); 23 | } catch(e) { 24 | console.warn("Unable to parse json:", this.raw); 25 | json = {}; 26 | } 27 | 28 | if(options.pretty) { 29 | fragment.raw = JSON.stringify(MirrorVerseAudioRouter.toVarv(json), null, 2); 30 | } else { 31 | fragment.raw = JSON.stringify(MirrorVerseAudioRouter.toVarv(json)); 32 | } 33 | fragment.auto = options.auto; 34 | 35 | return fragment.html[0]; 36 | } 37 | 38 | setupAutoDomHandling() { 39 | if(!this.supportsAutoDom()) { 40 | return; 41 | } 42 | 43 | let self = this; 44 | 45 | function sanitize(jsonRaw) { 46 | let json = JSON.parse(jsonRaw); 47 | 48 | if(json.unused != null) { 49 | json.unused.forEach((unused) => { 50 | if (unused.type === "DecisionNode") { 51 | delete json.nodes[unused.id]; 52 | } 53 | }); 54 | } 55 | 56 | return JSON.stringify(json, (key, value)=>{ 57 | if(key === "position") { 58 | return null; 59 | } 60 | if(key === "unused") { 61 | return null; 62 | } 63 | return value; 64 | }, 0); 65 | } 66 | 67 | this.registerOnFragmentChangedHandler((context) => { 68 | //Check if really updated, or just a position change 69 | let sanitizedNew = null; 70 | try { 71 | sanitizedNew = sanitize(self.raw); 72 | } catch(e) { 73 | console.warn(e); 74 | } 75 | 76 | let sanitizedOld = null; 77 | 78 | try { 79 | sanitizedOld = sanitize(this.oldRaw); 80 | } catch(e) { 81 | console.warn(e); 82 | } 83 | 84 | if(sanitizedOld !== sanitizedNew) { 85 | //Save new raw 86 | this.oldRaw = this.raw; 87 | self.autoDomDirty = true; 88 | if (self.auto && !Fragment.disableAutorun) { 89 | self.insertAutoDom(); 90 | } 91 | } 92 | }); 93 | } 94 | 95 | supportsAutoDom() { 96 | return true; 97 | } 98 | 99 | static type() { 100 | return "text/mirrorverse-audio-router"; 101 | } 102 | } 103 | window.AudioRouterFragment = AudioRouterFragment; 104 | MonacoEditor.registerExtraType(AudioRouterFragment.type(), "json"); 105 | Fragment.registerFragmentType(AudioRouterFragment); 106 | -------------------------------------------------------------------------------- /integration/audio-router/audio-router.js: -------------------------------------------------------------------------------- 1 | class MirrorVerseAudioRouter { 2 | static toVarv(json) { 3 | let outputVarv = {"concepts": {"audioManager":{"actions":{}}}}; 4 | 5 | let usedNames = new Set(); 6 | let uniqueIdMap = new Map(); 7 | let alreadySetup = new Set(); 8 | 9 | function getUniqueId(nodeId, nodeData) { 10 | if(uniqueIdMap.has(nodeId)) { 11 | return uniqueIdMap.get(nodeId); 12 | } 13 | 14 | let uniqueId = nodeId; 15 | 16 | if(nodeData.name != null && nodeData.name.length > 0) { 17 | uniqueId = nodeData.name; 18 | 19 | if(usedNames.has(uniqueId)) { 20 | let i = 1; 21 | while(usedNames.has(uniqueId+""+i)) { 22 | i++; 23 | } 24 | uniqueId = uniqueId+""+i; 25 | } 26 | } 27 | 28 | usedNames.add(uniqueId); 29 | uniqueIdMap.set(nodeId, uniqueId); 30 | 31 | return uniqueId; 32 | } 33 | 34 | function createConnectionNode(uniqueId, index, nodeId, rootName) { 35 | let connectionActions = []; 36 | outputVarv.concepts.audioManager.actions[uniqueId+"NodeConnection"+index] = connectionActions; 37 | 38 | if(typeof nodeId === "string") { 39 | let nodeData = json.nodes[nodeId]; 40 | let uniqueId = getUniqueId(nodeId, nodeData); 41 | 42 | if(nodeData.nodeType === "DecisionNode") { 43 | connectionActions.push(uniqueId+"NodeIn"); 44 | 45 | if(alreadySetup.has(nodeId)) { 46 | return; 47 | } 48 | alreadySetup.add(nodeId); 49 | 50 | createDecisionNode(nodeData, uniqueId, rootName); 51 | } else { 52 | //Value node 53 | connectionActions.push("selectOriginalAudioStream"); 54 | let setAction = {"set":{}}; 55 | 56 | let value = nodeData.value; 57 | 58 | if(rootName === "volume") { 59 | value = parseFloat(value); 60 | } 61 | 62 | setAction.set[rootName] = nodeData.value; 63 | connectionActions.push(setAction); 64 | } 65 | } 66 | } 67 | 68 | function createDecisionNode(nodeData, uniqueId, rootName) { 69 | let nodeInActions = []; 70 | outputVarv.concepts.audioManager.actions[uniqueId+"NodeIn"] = nodeInActions; 71 | 72 | //Select correct node 73 | switch(nodeData.concept) { 74 | case "client": 75 | nodeInActions.push("selectClient") 76 | break; 77 | case "currentRoom": 78 | nodeInActions.push("selectCurrentRoom") 79 | break; 80 | case "toolManager": 81 | nodeInActions.push("selectToolManager") 82 | break; 83 | default: 84 | console.warn("Unknown concept:", nodeData.concept); 85 | } 86 | 87 | //Push decisions 88 | for(let decisionIndex = 1; decisionIndex 2 | 3 | Name: 4 | Id: 5 | Concept: 6 | client 7 | currentRoom 8 | toolManager 9 | 10 | 11 | Property: 12 | 13 | 14 | Decisions: +- 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | Muted: 28 | Volume: 29 | Filter: nonemuffledoverdrive 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | includes 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | true 48 | false 49 | 50 | 51 | 52 | 53 | 54 | == 55 | != 56 | 57 | 58 | 59 | 60 | 61 | == 62 | != 63 | > 64 | < 65 | >= 66 | <= 67 | startsWith 68 | endsWith 69 | includes 70 | matches 71 | 72 | 73 | 74 | 75 | 76 | == 77 | != 78 | > 79 | < 80 | >= 81 | <= 82 | 83 | 84 | 85 | 86 | 87 | true 88 | false 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /integration/cauldron-delayloader/cauldron-delayloader.js: -------------------------------------------------------------------------------- 1 | // Run when/if Cauldron is initialized 2 | { 3 | let alreadyInitializedDelayLoader = false; 4 | EventSystem.registerEventCallback("Cauldron.OnOpen", async ()=>{ 5 | if (!alreadyInitializedDelayLoader){ 6 | console.log("Fetching additional Varv packages for Cauldron integration"); 7 | await wpm.require(["varv-cauldron", "varv-dom-highlight"]); 8 | 9 | // Cause a restart to update the TreeBrowser with the current Concepts 10 | EventSystem.triggerEvent("Varv.Restart"); 11 | alreadyInitializedDelayLoader = true; 12 | } 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /integration/cauldron-delayloader/descriptor.json: -------------------------------------------------------------------------------- 1 | { 2 | "friendlyName": "Varv Cauldron (delayed)", 3 | "description": "A delayed loader for the varv-cauldron package", 4 | "dependencies": [ 5 | "codestrates-repos #EventSystem" 6 | ], 7 | "optionalDependencies": [ 8 | "#varv-cauldron", 9 | "#varv-dom-highlight" 10 | ], 11 | "assets": [], 12 | "license": "MIT", 13 | "version": "0.1", 14 | "changelog": {} 15 | } 16 | -------------------------------------------------------------------------------- /integration/cauldron/CauldronDatastore.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CauldronDatastore 3 | * A datastore that allows mapping parts of concepts so that they show up in 4 | * the Cauldron TreeBrowser and are inspectable with the inspector 5 | * 6 | * This code is licensed under the MIT License (MIT). 7 | * 8 | * Copyright 2020, 2021, 2022 Rolf Bagge, Janus B. Kristensen, CAVI, 9 | * Center for Advanced Visualization and Interaction, Aarhus University 10 | * 11 | * Permission is hereby granted, free of charge, to any person obtaining a copy 12 | * of this software and associated documentation files (the “Software”), to deal 13 | * in the Software without restriction, including without limitation the rights 14 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | * copies of the Software, and to permit persons to whom the Software is 16 | * furnished to do so, subject to the following conditions: 17 | * 18 | * The above copyright notice and this permission notice shall be included in 19 | * all copies or substantial portions of the Software. 20 | * 21 | * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 27 | * THE SOFTWARE. 28 | * 29 | */ 30 | 31 | /** 32 | * 33 | */ 34 | class CauldronDatastore extends DirectDatastore { 35 | constructor(name, options = {}) { 36 | super(name, options); 37 | let self = this; 38 | 39 | this.destroyCallbacks = []; 40 | this.conceptAddedCallbacks = []; 41 | this.instanceAddedCallbacks = new Map(); 42 | this.instanceRemovedCallbacks = new Map(); 43 | 44 | this.appearCallback = VarvEngine.registerEventCallback("appeared", async (context)=> { 45 | let mark = VarvPerformance.start(); 46 | if (self.isConceptMapped(context.concept)){ 47 | let callbacks = self.instanceAddedCallbacks.get(context.concept.name); 48 | if (callbacks){ 49 | for (let callback of callbacks){ 50 | callback(context.target); 51 | } 52 | } 53 | } 54 | VarvPerformance.stop("CauldronDatastore.registerEventCallback.appeared", mark); 55 | }); 56 | this.disappearCallback = VarvEngine.registerEventCallback("disappeared", async (context)=> { 57 | if (self.isConceptMapped(context.concept)){ 58 | let callbacks = self.instanceRemovedCallbacks.get(context.concept.name); 59 | if (callbacks){ 60 | for (let callback of callbacks){ 61 | callback(context.target); 62 | } 63 | } 64 | } 65 | }); 66 | } 67 | 68 | isShared() { 69 | return false; 70 | } 71 | 72 | destroy() { 73 | this.appearCallback.delete(); 74 | this.disappearCallback.delete(); 75 | this.engineReloadedCallback.delete(); 76 | for (let callback of this.destroyCallbacks){ 77 | callback(this); 78 | } 79 | } 80 | 81 | registerDestroyCallback(callback){ 82 | this.destroyCallbacks.push(callback); 83 | return callback; 84 | } 85 | 86 | async init() { 87 | let self = this; 88 | 89 | this.engineReloadedCallback = VarvEngine.registerEventCallback("engineReloaded", ()=>{ 90 | console.log("Engine reloaded, register on trees!"); 91 | // Inform trees about us 92 | for (let tree of window.ConceptTreeGenerator.instances){ 93 | tree.onAddDatastore(this); 94 | } 95 | 96 | // Listen for new ones 97 | EventSystem.registerEventCallback("Varv.ConceptTreeGeneratorSpawned", (evt)=>{ 98 | evt.detail.onAddDatastore(self); 99 | }); 100 | }); 101 | } 102 | 103 | registerConceptAddedCallback(callback){ 104 | this.conceptAddedCallbacks.push(callback); 105 | 106 | // Pre-feed with currently mapped 107 | for (const conceptName of this.mappedConcepts.keys()){ 108 | callback(VarvEngine.getConceptFromType(conceptName)); 109 | } 110 | 111 | return callback; 112 | } 113 | 114 | async registerConceptInstanceAddedCallback(concept, callback){ 115 | if (!this.instanceAddedCallbacks.has(concept.name)){ 116 | this.instanceAddedCallbacks.set(concept.name, []); 117 | } 118 | 119 | let callbacks = this.instanceAddedCallbacks.get(concept.name); 120 | callbacks.push(callback); 121 | 122 | // Pre-feed with currently mapped 123 | for (const conceptUUID of await VarvEngine.getAllUUIDsFromType(concept.name)){ 124 | callback(conceptUUID); 125 | } 126 | 127 | return callback; 128 | } 129 | 130 | removeConceptInstanceAddedCallback(concept, callback){ 131 | let callbacks = this.instanceAddedCallbacks.get(concept.name); 132 | if (!callbacks) { 133 | console.log("Cauldron: Tried to remove a concept callback but couldn't", concept.name, callback); 134 | return; 135 | }; 136 | this.instanceAddedCallbacks.set(concept.name, callbacks.filter(e => e !== callback)); 137 | } 138 | 139 | registerConceptInstanceRemovedCallback(concept, callback){ 140 | if (!this.instanceRemovedCallbacks.has(concept.name)){ 141 | this.instanceRemovedCallbacks.set(concept.name, []); 142 | } 143 | 144 | let callbacks = this.instanceRemovedCallbacks.get(concept.name); 145 | callbacks.push(callback); 146 | 147 | return callback; 148 | } 149 | 150 | removeConceptInstanceRemovedCallback(concept, callback){ 151 | let callbacks = this.instanceRemovedCallbacks.get(concept.name); 152 | if (!callbacks) { 153 | console.log("Cauldron: Tried to remove a concept removed callback but couldn't", concept.name, callback); 154 | return; 155 | }; 156 | this.instanceRemovedCallbacks.set(concept.name, callbacks.filter(e => e !== callback)); 157 | } 158 | 159 | createBackingStore(concept, property) { 160 | const self = this; 161 | 162 | if (this.isPropertyMapped(concept,property)){ 163 | console.log("Already mapped property"); 164 | return; 165 | } 166 | 167 | if (!this.isConceptMapped(concept)){ 168 | // Concept add update 169 | if(CauldronDatastore.DEBUG) { 170 | console.log("New concept", concept); 171 | } 172 | for (let callback of self.conceptAddedCallbacks){ 173 | callback(concept); 174 | } 175 | } 176 | 177 | // Check if concept already is mapped, if not, register it 178 | this.internalAddPropertyMapping(concept, property, {}); 179 | } 180 | 181 | removeBackingStore(concept, property) { 182 | if (!this.isPropertyMapped(concept, property)) 183 | throw 'Cannot unmap property from memory because the property was not mapped: ' + concept + "." + property; 184 | 185 | // TODO: Remove concept update 186 | 187 | this.internalRemovePropertyMapping(concept, property); 188 | } 189 | 190 | loadBackingStore() { 191 | // No storage, do nothing 192 | } 193 | } 194 | 195 | window.CauldronDatastore = CauldronDatastore; 196 | 197 | // Register default cauldron datastore 198 | Datastore.registerDatastoreType("cauldron", CauldronDatastore); 199 | -------------------------------------------------------------------------------- /integration/cauldron/ConceptDecorator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ConceptDecorator - decorates Concept nodes in TreeBrowser trees 3 | * 4 | * This code is licensed under the MIT License (MIT). 5 | * 6 | * Copyright 2020, 2021, 2022 Rolf Bagge, Janus B. Kristensen, CAVI, 7 | * Center for Advanced Visualization and Interaction, Aarhus University 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the “Software”), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in 17 | * all copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | * THE SOFTWARE. 26 | * 27 | */ 28 | 29 | /** 30 | * 31 | */ 32 | class ConceptDecorator { 33 | /** 34 | * Attempts to decorate the given TreeNode 35 | * @param {TreeNode} node - The node to decorate 36 | * @returns {boolean} True/False depending on if the node was decorated 37 | */ 38 | static decorate(node) { 39 | if(node.type === "ConceptNode") { 40 | node.setProperty("content", node.context.name); 41 | node.setProperty("icon", IconRegistry.createIcon("mdc:api")); 42 | return true; 43 | } else if(node.type === "ConceptInstanceNode") { 44 | node.setProperty("content", node.context.uuid); 45 | node.setProperty("icon", IconRegistry.createIcon("mdc:class")); 46 | return true; 47 | } else if(node.type === "DatastoreNode") { 48 | node.setProperty("content", node.context.name); 49 | node.setProperty("icon", IconRegistry.createIcon("mdc:circle")); 50 | return true; 51 | } else if(node.type === "ConceptRootNode") { 52 | node.setProperty("content", "Concepts"); 53 | node.setProperty("icon", IconRegistry.createIcon("mdc:all_out")); 54 | return true; 55 | } 56 | 57 | return false; 58 | } 59 | 60 | /** 61 | * Attempts to decorate the given DataTransfer based on the given TreeNode 62 | * @param {TreeNode} node 63 | * @param {DataTransfer} dataTransfer 64 | * @returns {boolean} True/False depending on if the DataTransfer was decorated 65 | */ 66 | static decorateDataTransfer(node, dataTransfer) { 67 | return false; 68 | } 69 | } 70 | 71 | window.ConceptDecorator = ConceptDecorator; 72 | 73 | TreeGenerator.registerDecorator(ConceptDecorator, 10); 74 | -------------------------------------------------------------------------------- /integration/cauldron/ConceptMenuActions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ConceptTreeGenerator - Generate program/state structure to explore in Cauldron TreeBrowser 3 | * 4 | * This code is licensed under the MIT License (MIT). 5 | * 6 | * Copyright 2020, 2021, 2022 Rolf Bagge, Janus B. Kristensen, CAVI, 7 | * Center for Advanced Visualization and Interaction, Aarhus University 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the “Software”), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in 17 | * all copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | * THE SOFTWARE. 26 | * 27 | */ 28 | 29 | MenuSystem.MenuManager.registerMenuItem("TreeBrowser.TreeNode.ContextMenu", { 30 | label: "Create Instance", 31 | group: "ConceptActions", 32 | groupOrder: 0, 33 | icon: IconRegistry.createIcon("mdc:class"), 34 | onOpen: (menu)=>{ 35 | return menu.context.type == "ConceptNode"; 36 | }, 37 | onAction: async (menuItem) =>{ 38 | let id = await menuItem.menu.context.context.create(); 39 | setTimeout(()=>{ 40 | let treeBrowser = menuItem.menu.context.getTreeBrowser(); 41 | let treeNodes = treeBrowser.findTreeNode(id); 42 | if(treeNodes.length > 0) { 43 | let treeNode = treeNodes[0]; 44 | treeNode.reveal(); 45 | treeNode.select(); 46 | } 47 | }, 0); 48 | } 49 | }); 50 | 51 | MenuSystem.MenuManager.registerMenuItem("TreeBrowser.TreeNode.ContextMenu", { 52 | label: "Delete", 53 | group: "ConceptActions", 54 | groupOrder: 0, 55 | icon: IconRegistry.createIcon("mdc:delete"), 56 | onOpen: (menu)=>{ 57 | return menu.context.type == "ConceptInstanceNode"; 58 | }, 59 | onAction: async (menuItem) =>{ 60 | menuItem.menu.context.context.concept.delete(menuItem.menu.context.context.uuid); 61 | } 62 | }); 63 | 64 | MenuSystem.MenuManager.registerMenuItem("TreeBrowser.TreeNode.ContextMenu", { 65 | label: "Clone", 66 | group: "ConceptActions", 67 | groupOrder: 0, 68 | icon: IconRegistry.createIcon("mdc:copy"), 69 | onOpen: (menu)=>{ 70 | return menu.context.type == "ConceptInstanceNode"; 71 | }, 72 | onAction: async (menuItem) =>{ 73 | menuItem.menu.context.context.concept.clone(menuItem.menu.context.context.uuid); 74 | } 75 | }); 76 | 77 | MenuSystem.MenuManager.registerMenuItem("TreeBrowser.TreeNode.ContextMenu", { 78 | label: "Clone (Deep)", 79 | group: "ConceptActions", 80 | groupOrder: 0, 81 | icon: IconRegistry.createIcon("mdc:copy"), 82 | onOpen: (menu)=>{ 83 | return menu.context.type == "ConceptInstanceNode"; 84 | }, 85 | onAction: async (menuItem) =>{ 86 | menuItem.menu.context.context.concept.clone(menuItem.menu.context.context.uuid, true); 87 | } 88 | }); 89 | 90 | MenuSystem.MenuManager.registerMenuItem("TreeBrowser.TreeNode.ContextMenu", { 91 | label: "Copy ID", 92 | group: "ConceptActions", 93 | groupOrder: 0, 94 | icon: IconRegistry.createIcon("mdc:copy"), 95 | onOpen: (menu)=>{ 96 | return menu.context.type == "ConceptInstanceNode"; 97 | }, 98 | onAction: async (menuItem) =>{ 99 | await navigator.clipboard.writeText(menuItem.menu.context.context.uuid); 100 | } 101 | }); 102 | 103 | MenuSystem.MenuManager.registerMenuItem("Cauldron.Help.Documentation", { 104 | label: "Varv", 105 | icon: IconRegistry.createIcon("webstrates:varv"), 106 | onAction: () => { 107 | window.open("https://varv.projects.cavi.au.dk/api/varv/"); 108 | } 109 | }); 110 | 111 | //Setup cauldron menu item 112 | MenuSystem.MenuManager.registerMenuItem("Cauldron.Editor.Toolbar", { 113 | label: "JSON/YAML", 114 | icon: IconRegistry.createIcon("mdc:cached"), 115 | group: "EditActions", 116 | groupOrder: 0, 117 | order: 200, 118 | onOpen: (menu) => { 119 | return menu.context instanceof ConceptDefinitionFragment; 120 | }, 121 | onAction: (menuItem) => { 122 | EventSystem.triggerEvent("Varv.Convert.YAMLJSON", { 123 | fragment: Fragment.one(menuItem.menu.context) 124 | }); 125 | } 126 | }); 127 | 128 | EventSystem.registerEventCallback("Varv.Convert.YAMLJSON", async (evt)=>{ 129 | let fragment = evt.detail.fragment; 130 | let code = fragment.raw; 131 | 132 | let convertedCode = YAMLJSONConverter.convert(code); 133 | fragment.raw = convertedCode; 134 | 135 | const detail = { 136 | fragment: evt.detail.fragment, 137 | }; 138 | 139 | EventSystem.triggerEvent("Cauldron.Open.FragmentEditor", detail); 140 | }); 141 | -------------------------------------------------------------------------------- /integration/cauldron/ConceptTreeGenerator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ConceptDecorator - decorates Concept nodes in TreeBrowser trees 3 | * 4 | * This code is licensed under the MIT License (MIT). 5 | * 6 | * Copyright 2020, 2021, 2022 Rolf Bagge, Janus B. Kristensen, CAVI, 7 | * Center for Advanced Visualization and Interaction, Aarhus University 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the “Software”), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in 17 | * all copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | * THE SOFTWARE. 26 | * 27 | */ 28 | 29 | /** 30 | * 31 | */ 32 | class ConceptTreeGenerator extends TreeGenerator { 33 | constructor(parentNode) { 34 | super(); 35 | 36 | this.rootNode = new TreeNode({ 37 | type: "ConceptRootNode", 38 | context: null 39 | }); 40 | TreeGenerator.decorateNode(this.rootNode); 41 | parentNode.addNode(this.rootNode); 42 | } 43 | 44 | onAddDatastore(datastore){ 45 | let activeElement = document.activeElement; 46 | 47 | let self = this; 48 | let datastoreNode = new TreeNode({ 49 | type: "DatastoreNode", 50 | lookupKey: datastore.name, 51 | context: datastore 52 | }); 53 | TreeGenerator.decorateNode(datastoreNode); 54 | this.rootNode.addNode(datastoreNode); 55 | this.rootNode.unfold(); 56 | 57 | datastore.registerDestroyCallback(()=>{ 58 | self.destroyNode(datastoreNode); 59 | }); 60 | 61 | // Concepts 62 | datastore.registerConceptAddedCallback((concept)=>{ 63 | datastoreNode.unfold(); 64 | 65 | let conceptNode = new TreeNode({ 66 | type: "ConceptNode", 67 | lookupKey: concept.name, 68 | context: concept 69 | }); 70 | 71 | // Instances 72 | let instanceAddedCallback = datastore.registerConceptInstanceAddedCallback(concept, (uuid)=>{ 73 | let node = self.addInstanceNode(datastore, conceptNode, concept, uuid); 74 | let instanceRemovedCallback = datastore.registerConceptInstanceRemovedCallback(concept, (removedUUID)=>{ 75 | if (removedUUID===uuid){ 76 | self.destroyNode(node); 77 | } 78 | }); 79 | node.cleanup = [()=>{ 80 | datastore.removeConceptInstanceRemovedCallback(concept, instanceRemovedCallback); 81 | }]; 82 | }); 83 | 84 | TreeGenerator.decorateNode(conceptNode); 85 | datastoreNode.addNode(conceptNode); 86 | }); 87 | 88 | // STUB: Concepts cannot be removed right now but should be cleaned up here 89 | // datastore.removeConceptInstanceAddedCallback(concept, instanceAddedCallback); 90 | 91 | } 92 | 93 | destroyNode(node){ 94 | node.parentNode.removeNode(node); 95 | 96 | // Run cleanup 97 | if (node.cleanup){ 98 | for (let entry of node.cleanup){ 99 | entry(); 100 | } 101 | } 102 | 103 | // Destroy children 104 | for (let child of Array.from(node.childNodes)){ // copy to avoid concurrent mods 105 | this.destroyNode(child); 106 | } 107 | } 108 | 109 | addInstanceNode(datastore, parentNode, concept, uuid){ 110 | let instanceNode = new TreeNode({ 111 | type: "ConceptInstanceNode", 112 | lookupKey: uuid, 113 | context: {concept, uuid, datastore} 114 | }); 115 | TreeGenerator.decorateNode(instanceNode); 116 | parentNode.addNode(instanceNode); 117 | return instanceNode; 118 | } 119 | } 120 | 121 | 122 | EventSystem.registerEventCallback("Cauldron.TreeBrowserSpawned", ({detail: {root: rootNode}})=>{ 123 | let generator = new ConceptTreeGenerator(rootNode); 124 | EventSystem.triggerEvent("Varv.ConceptTreeGeneratorSpawned", generator); 125 | window.ConceptTreeGenerator.instances.push(generator); 126 | }); 127 | 128 | window.ConceptTreeGenerator = ConceptTreeGenerator; 129 | window.ConceptTreeGenerator.instances = []; 130 | -------------------------------------------------------------------------------- /integration/cauldron/base.scss: -------------------------------------------------------------------------------- 1 | /* STUB: Not a proper MDC select popup, does not follow layouting guide */ 2 | .cauldron-inspector-element-autocomplete { 3 | border: 1px dotted #ccc; 4 | padding: 3px; 5 | position: fixed; 6 | z-index: 9000; 7 | background: var(--mdc-theme-surface, white); 8 | margin-top: 2em; 9 | 10 | &.hidden { 11 | display: none; 12 | } 13 | 14 | ul { 15 | list-style-type: none; 16 | padding: 0; 17 | margin: 0; 18 | 19 | li { 20 | padding: 5px 0; 21 | cursor: pointer; 22 | 23 | &:hover { 24 | background: #eee; 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /integration/cauldron/descriptor.json: -------------------------------------------------------------------------------- 1 | { 2 | "friendlyName": "Varv Cauldron Datastore", 3 | "description": "A datastore that allows mapping parts of concepts so that they show up in the Cauldron TreeBrowser and can be inspected with the inspector", 4 | "dependencies": [ 5 | "#varv-engine", 6 | "cauldron-repos #Cauldron" 7 | ], 8 | "assets": [], 9 | "license": "MIT", 10 | "version": "0.1", 11 | "changelog": {} 12 | } 13 | -------------------------------------------------------------------------------- /integration/varvscript/VarvScriptFragment.js: -------------------------------------------------------------------------------- 1 | /** 2 | * VarvScriptFragment - A simpler scripting language for Varv 3 | * 4 | * This code is licensed under the MIT License (MIT). 5 | * 6 | * Copyright 2020, 2021, 2022 Rolf Bagge, Janus B. Kristensen, CAVI, 7 | * Center for Advanced Visualization and Interaction, Aarhus University 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the “Software”), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in 17 | * all copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | * THE SOFTWARE. 26 | * 27 | */ 28 | 29 | wpm.onRemoved(()=>{ 30 | Fragment.unRegisterFragmentType(VarvScriptFragment); 31 | }); 32 | /** 33 | * A fragment that contains Varv code 34 | * 35 | * Supports auto - executes require() on load 36 | * @extends Fragments.Fragment 37 | * @hideconstructor 38 | * @memberof Fragments 39 | */ 40 | class VarvScriptFragment extends Fragment { 41 | constructor(html) { 42 | super(html); 43 | } 44 | 45 | async require(options = {}) { 46 | console.log("Trying to parse",this.raw); 47 | let result = parseVarvScript(this.raw); 48 | console.log("Parsed to ", result); 49 | return result; 50 | } 51 | 52 | supportsAuto() { 53 | return true; 54 | } 55 | 56 | static type() { 57 | return "text/varvscript"; 58 | } 59 | }; 60 | window.VarvScriptFragment = VarvScriptFragment; 61 | Fragment.registerFragmentType(VarvScriptFragment); -------------------------------------------------------------------------------- /integration/varvscript/descriptor.json: -------------------------------------------------------------------------------- 1 | { 2 | "friendlyName": "VarvScript Fragment", 3 | "description": "A simpler, less JSON-based way of scripting Varv code", 4 | "dependencies": [ 5 | "#varv-engine" 6 | ], 7 | "assets": [], 8 | "license": "MIT", 9 | "version": "0.1", 10 | "changelog": {} 11 | } 12 | -------------------------------------------------------------------------------- /jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["plugins/markdown"], 3 | "source": { 4 | "include": ["."] 5 | }, 6 | "opts": { 7 | "recurse": true, 8 | "destination": "./jsdoc/", 9 | "readme": "README.md" 10 | }, 11 | "docdash": { 12 | "sort": true, 13 | "collapse": true, // Collapse navigation by default except current object's navigation of the current page, top for top level collapse 14 | "sectionOrder": [ // Order the main section in the navbar (default order shown here) 15 | "Namespaces", 16 | "Classes", 17 | "Modules", 18 | "Externals", 19 | "Events", 20 | "Mixins", 21 | "Tutorials", 22 | "Interfaces" 23 | ] 24 | } 25 | } -------------------------------------------------------------------------------- /prototypes/README.md: -------------------------------------------------------------------------------- 1 | # Varv prototypes 2 | 3 | To try a Varv prototype construct your URL as follows: 4 | https:///new?prototypeUrl= 5 | 6 | * varv - An empty Varv project with an editor to get started easily -------------------------------------------------------------------------------- /prototypes/varv.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | New Varv 5 | 6 | 51 | 52 | 53 | 54 | 55 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | await VarvEngine.start(); 68 | 69 | { 70 | "concepts": { 71 | "myConcept": { 72 | "schema": { 73 | "myProperty": "string" 74 | }, 75 | "actions": { 76 | "myAction": { 77 | "when": { 78 | "click": "myView" 79 | }, 80 | "then": [ 81 | "debugContext" 82 | ] 83 | } 84 | } 85 | } 86 | } 87 | } 88 | 89 | <dom-view-template> 90 | <div concept="myConcept"> 91 | <button view="myView">Click me!</button> 92 | </div> 93 | </dom-view-template> 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /prototypes/varv.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Webstrates/Varv/1a2b3f5c33acb2c4618418aa012573e6008c6d15/prototypes/varv.zip -------------------------------------------------------------------------------- /triggers/FlowTriggers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * FlowTriggers - Triggers based on control flow and calls 3 | * 4 | * This code is licensed under the MIT License (MIT). 5 | * 6 | * Copyright 2020, 2021, 2022 Rolf Bagge, Janus B. Kristensen, CAVI, 7 | * Center for Advanced Visualization and Interaction, Aarhus University 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the “Software”), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in 17 | * all copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | * THE SOFTWARE. 26 | * 27 | */ 28 | 29 | /** 30 | * A trigger 'action' that triggers when an action is run 31 | * @memberOf Triggers 32 | * @example 33 | * //Trigger before action 'myActionName' is run 34 | * { 35 | * "action": { 36 | * "action": "myActionName", 37 | * "hook": "before" 38 | * } 39 | * } 40 | * 41 | * //Trigger after action 'myActionName' is run 42 | * { 43 | * "action": { 44 | * "action": "myActionName", 45 | * "hook": "after" 46 | * } 47 | * } 48 | * 49 | * //Trigger after any of multiple actions are run 50 | * { 51 | * "action": { 52 | * "action": ["myActionName", "mySecondActionName", "myThirdActionName"], 53 | * "hook": "after" 54 | * } 55 | * } 56 | * //Shorthand, trigger after 'myActionName' is run 57 | * { 58 | * "action": "myActionName" 59 | * } 60 | * 61 | * //Shorthand, trigger after 'myConcept.myActionName' is run 62 | * { 63 | * "action": "myConcept.myActionName" 64 | * } 65 | * 66 | * //Super shorthand, triggers after myActionName 67 | * "myActionName" 68 | */ 69 | class ActionTrigger extends Trigger { 70 | static options() { 71 | return { 72 | "action": "string", 73 | "hook": "enum[before,after]%after" 74 | } 75 | } 76 | 77 | constructor(name, options, concept) { 78 | if (typeof options === "string") { 79 | options = { 80 | action: options 81 | } 82 | } 83 | 84 | let defaultOptions = { 85 | "hook": "after" 86 | }; 87 | 88 | options = Object.assign({}, defaultOptions, options); 89 | 90 | super(name, options, concept); 91 | } 92 | 93 | enable() { 94 | const self = this; 95 | 96 | this.deleteTrigger = Trigger.registerTriggerEvent("action", async (contexts)=>{ 97 | //Only 1 context 98 | let context = contexts[0]; 99 | 100 | let actions = self.options.action; 101 | 102 | if(!Array.isArray(actions)) { 103 | actions = [actions]; 104 | } 105 | 106 | for(let actionEntry of actions) { 107 | let actionPart = actionEntry; 108 | let conceptPart = null; 109 | 110 | let split = actionEntry.split("."); 111 | if(split.length === 2) { 112 | //Action was on the form, concept.action 113 | actionPart = split[1]; 114 | conceptPart = split[0]; 115 | } 116 | 117 | if(conceptPart != null && context.actionConcept !== conceptPart) { 118 | //Not the concept we are looking for, skip 119 | continue; 120 | } 121 | 122 | if(context.actionName !== actionPart) { 123 | //Not the action we are looking for, skip 124 | continue; 125 | } 126 | 127 | if(context.hook !== self.options.hook) { 128 | //Not the hook we are looking at, skip 129 | continue; 130 | } 131 | 132 | let clonedContexts = Action.cloneContext(context.actionContext); 133 | 134 | await Trigger.trigger(self.name, clonedContexts); 135 | } 136 | }); 137 | } 138 | 139 | disable() { 140 | if(this.deleteTrigger != null) { 141 | this.deleteTrigger.delete(); 142 | } 143 | } 144 | 145 | static async before(action, contexts) { 146 | await ActionTrigger.doTrigger(action, contexts, false); 147 | } 148 | 149 | static async after(action, contexts) { 150 | await ActionTrigger.doTrigger(action, contexts, true); 151 | } 152 | 153 | static async doTrigger(action, contexts, after) { 154 | await Trigger.trigger("action", { 155 | target: null, 156 | actionContext: contexts, 157 | actionName: action.name, 158 | actionConcept: action.concept?.name, 159 | hook: after?"after":"before" 160 | }); 161 | } 162 | } 163 | Trigger.registerTrigger("action", ActionTrigger); 164 | window.ActionTrigger = ActionTrigger; 165 | -------------------------------------------------------------------------------- /triggers/PropertyTriggers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * PropertyTriggers - triggers on property changes 3 | * 4 | * This code is licensed under the MIT License (MIT). 5 | * 6 | * Copyright 2020, 2021, 2022 Rolf Bagge, Janus B. Kristensen, CAVI, 7 | * Center for Advanced Visualization and Interaction, Aarhus University 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the “Software”), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in 17 | * all copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | * THE SOFTWARE. 26 | * 27 | */ 28 | 29 | /** 30 | * A trigger "stateChanged" that listens for property state changes 31 | * @memberOf Triggers 32 | * @example 33 | * //Triggers the stateChanged event when myProperty has changed, only the first concept that has the property is checked. 34 | * { 35 | * "stateChanged": "myProperty" 36 | * } 37 | * 38 | * @example 39 | * //Triggers the stateChanged event when any property on myConcept changes 40 | * { 41 | * "stateChanged": "myConcept" 42 | * } 43 | * 44 | * @example 45 | * //Triggers the stateChanged event when any property on myConcept changes, or if myProperty changes, on the first concept it was found on 46 | * { 47 | * "stateChanged": ["myConcept", "myProperty"] 48 | * } 49 | * 50 | * @example 51 | * //Triggers the stateChanged event when property myProperty changes on myConcept 52 | * { 53 | * "stateChanged": {"myConcept": "myProperty"} 54 | * } 55 | * 56 | * @example 57 | * //Triggers the stateChanged event when property myProperty changes on myConcept 58 | * { 59 | * "stateChanged": { 60 | * "concept": "myConcept", 61 | * "property": "myProperty" 62 | * } 63 | * } 64 | * 65 | * @example 66 | * //Triggers the stateChanged event when property myProperty changes 67 | * { 68 | * "stateChanged": { 69 | * "property": "myProperty" 70 | * } 71 | * } 72 | * 73 | * @example 74 | * //Triggers the stateChanged event when a property on myConcept changes 75 | * { 76 | * "stateChanged": { 77 | * "concept": "myConcept" 78 | * } 79 | * } 80 | */ 81 | class StateChangedTrigger extends Trigger { 82 | constructor(name, options, concept) { 83 | if(typeof options === "string") { 84 | //Shorthand options string 85 | options = { 86 | runtimeLookup: [options] 87 | }; 88 | } else if(Array.isArray(options)) { 89 | //Shorthand options concept array 90 | options = { 91 | runtimeLookup: [] 92 | } 93 | for(let i = 0; i{ 124 | //Always only 1 entry in array 125 | context = context[0]; 126 | 127 | let options = Object.assign({}, self.options); 128 | 129 | if(options.runtimeLookup != null) { 130 | let lookedUpReferences = []; 131 | options.runtimeLookup.forEach((reference)=>{ 132 | let lookup = VarvEngine.lookupReference(reference, self.concept); 133 | lookedUpReferences.push(lookup); 134 | }); 135 | 136 | //Set options to the looked up references 137 | options = Object.assign(options, lookedUpReferences); 138 | } 139 | 140 | //Check if options array shorthand 141 | if(Array.isArray(options)) { 142 | let temp = options; 143 | options = { 144 | concept: [], 145 | property: [] 146 | } 147 | 148 | temp.forEach((entry)=>{ 149 | if(entry.concept != null) { 150 | options.concept.push(entry.concept); 151 | } 152 | 153 | if(entry.property != null) { 154 | options.property.push(entry.property); 155 | } 156 | }) 157 | } 158 | 159 | let clonedContext = Action.cloneContext(context); 160 | 161 | if(Trigger.DEBUG) { 162 | console.log("StateChangedTrigger:", self.name, options, ""+context.target); 163 | } 164 | 165 | let triggeringConcept = await VarvEngine.getConceptFromUUID(context.target); 166 | 167 | if(triggeringConcept == null) { 168 | throw new Error("Unknown concept for UUID: "+context.target); 169 | } 170 | 171 | if(options.exactConceptMatch && options.concept == null) { 172 | //We are matching excact on concept, but have no concept, use owning concept 173 | options.concept = self.concept.name; 174 | } 175 | 176 | if(options.concept != null) { 177 | let filterConcepts = options.concept; 178 | if(!Array.isArray(filterConcepts)) { 179 | filterConcepts = [filterConcepts]; 180 | } 181 | 182 | let found = filterConcepts.length === 0; 183 | 184 | for(let filterConcept of filterConcepts) { 185 | if(options.exactConceptMatch) { 186 | if (triggeringConcept.name === filterConcept) { 187 | found = true; 188 | break; 189 | } 190 | } else { 191 | if (triggeringConcept.isA(filterConcept)) { 192 | found = true; 193 | break; 194 | } 195 | } 196 | } 197 | 198 | if(!found) { 199 | //Skip based on wrong concept 200 | if(Trigger.DEBUG) { 201 | console.log("Skipping based on wrong concept"); 202 | } 203 | return; 204 | } 205 | } 206 | 207 | if(options.property != null) { 208 | let filterProperties = options.property; 209 | if(!Array.isArray(filterProperties)) { 210 | filterProperties = [filterProperties]; 211 | } 212 | 213 | let found = filterProperties.length === 0; 214 | 215 | for(let filterProperty of filterProperties) { 216 | if(context.property === filterProperty) { 217 | found = true; 218 | break; 219 | } 220 | } 221 | 222 | if(!found) { 223 | //Skip based on wrong property 224 | if (Trigger.DEBUG) { 225 | console.log("Skipping based on wrong property") 226 | } 227 | return; 228 | } 229 | } 230 | 231 | await Trigger.trigger(self.name, clonedContext); 232 | }); 233 | } 234 | 235 | disable() { 236 | if(this.triggerDelete != null) { 237 | this.triggerDelete.delete(); 238 | } 239 | this.triggerDelete = null; 240 | } 241 | } 242 | Trigger.registerTrigger("stateChanged", StateChangedTrigger); 243 | window.StateChangedTrigger = StateChangedTrigger; 244 | -------------------------------------------------------------------------------- /triggers/TimingTriggers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Timing Triggers - triggers based on time 3 | * 4 | * This code is licensed under the MIT License (MIT). 5 | * 6 | * Copyright 2020, 2021, 2022 Rolf Bagge, Janus B. Kristensen, CAVI, 7 | * Center for Advanced Visualization and Interaction, Aarhus University 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the “Software”), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in 17 | * all copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | * THE SOFTWARE. 26 | * 27 | */ 28 | 29 | /** 30 | * A trigger 'interval' that triggers at a given interval, this trigger event has no target. 31 | * @memberOf Triggers 32 | * @example 33 | * //Trigger every 10 seconds 34 | * { 35 | * "interval": 10 36 | * } 37 | */ 38 | class IntervalTrigger extends Trigger { 39 | static options() { 40 | return { 41 | "interval": "number" 42 | } 43 | } 44 | 45 | constructor(name, options, concept) { 46 | //Shorthand 47 | if(typeof options === "number") { 48 | options = { 49 | interval: options 50 | } 51 | } 52 | 53 | super(name, options, concept); 54 | 55 | this.intervalId = null; 56 | } 57 | 58 | enable() { 59 | const self = this; 60 | 61 | let interval = this.options.interval; 62 | 63 | let currentRepetition = 0; 64 | 65 | this.intervalId = setInterval(async ()=>{ 66 | await Trigger.trigger(self.name, { 67 | target: null, 68 | variables:{ 69 | repetition: currentRepetition 70 | } 71 | }); 72 | 73 | currentRepetition++; 74 | }, interval); 75 | } 76 | 77 | disable() { 78 | if(this.intervalId != null) { 79 | clearInterval(this.intervalId); 80 | } 81 | this.intervalId = null; 82 | } 83 | } 84 | Trigger.registerTrigger("interval", IntervalTrigger); 85 | window.IntervalTrigger = IntervalTrigger; 86 | -------------------------------------------------------------------------------- /triggers/descriptor.json: -------------------------------------------------------------------------------- 1 | { 2 | "friendlyName": "Varv Standard Triggers", 3 | "description": "Simple built-in triggers for varv", 4 | "dependencies": [ 5 | "#varv-engine" 6 | ], 7 | "assets": [], 8 | "license": "MIT", 9 | "version": "0.1", 10 | "changelog": {} 11 | } 12 | -------------------------------------------------------------------------------- /views/dom/dom-highlight/DOMHighlight.js: -------------------------------------------------------------------------------- 1 | class DOMHighlighter { 2 | constructor(){ 3 | let self = this; 4 | 5 | let conceptHighlightCallback = EventSystem.registerEventCallback("Varv.DOMView.HighlightConcept", (evt)=>{ 6 | let concept = evt.detail; 7 | self.getViews().forEach((view)=>{ 8 | self.walkView(view, self.clearHighlight); 9 | self.walkView(view, function highlightConcept(node){ 10 | for (let conceptBinding of DOMView.singleton.getConceptPath(node)){ 11 | if (conceptBinding.concept===concept){ 12 | self.highlight(node); 13 | return false; // Don't highlight into children 14 | } 15 | } 16 | return true; 17 | }); 18 | }); 19 | }); 20 | 21 | let instanceHighlightCallback = EventSystem.registerEventCallback("Varv.DOMView.HighlightInstance", (evt)=>{ 22 | let uuid = evt.detail; 23 | self.getViews().forEach((view)=>{ 24 | self.walkView(view, self.clearHighlight); 25 | self.walkView(view, function highlightInstance(node){ 26 | for (let conceptBinding of DOMView.singleton.getConceptPath(node)){ 27 | if (conceptBinding.uuid===uuid){ 28 | self.highlight(node); 29 | return false; // Don't highlight into children 30 | } 31 | } 32 | return true; 33 | }); 34 | }); 35 | }); 36 | 37 | let propertyHighlightCallback = EventSystem.registerEventCallback("Varv.DOMView.HighlightProperty", (evt)=>{ 38 | let property = evt.detail; 39 | self.getViews().forEach((view)=>{ 40 | self.walkView(view, self.clearHighlight); 41 | self.walkView(view, function highlightProperty(node){ 42 | for (let entry of DOMView.singleton.getPropertyPath(node)){ 43 | if (entry.property===property){ 44 | if (node.setAttribute){ 45 | node.setAttribute("varv-domview-highlight",true); 46 | } 47 | return false; 48 | } 49 | } 50 | return true; 51 | }); 52 | }); 53 | }); 54 | 55 | let clearHighlightCallback = EventSystem.registerEventCallback("Varv.DOMView.ClearHighlights", (evt)=>{ 56 | self.getViews().forEach((view)=>{ 57 | self.walkView(view, self.clearHighlight); 58 | }); 59 | }); 60 | } 61 | 62 | getViews(){ 63 | return document.querySelectorAll("varv-view"); 64 | } 65 | 66 | highlight(node){ 67 | if (node.setAttribute){ 68 | node.setAttribute("varv-domview-highlight",true); 69 | } 70 | } 71 | 72 | clearHighlight(node){ 73 | if (node.getAttribute && node.getAttribute("varv-domview-highlight")){ 74 | node.removeAttribute("varv-domview-highlight"); 75 | } 76 | return true; 77 | } 78 | 79 | walkView(view, nodeCallback){ 80 | let diveIntoChildren = nodeCallback(view); 81 | if (diveIntoChildren){ 82 | for (let child of view.childNodes){ 83 | this.walkView(child, nodeCallback); 84 | } 85 | } 86 | } 87 | } 88 | 89 | window.DOMHighlighter = DOMHighlighter; 90 | window.DOMHighlighter.singleton = new DOMHighlighter(); -------------------------------------------------------------------------------- /views/dom/dom-highlight/descriptor.json: -------------------------------------------------------------------------------- 1 | { 2 | "friendlyName": "Varv DOM Highlighting", 3 | "description": "Allows applications like Cauldron to highlight concepts, properties and similar directly in the DOMView", 4 | "dependencies": [ 5 | "#varv-engine" 6 | ], 7 | "assets": [], 8 | "license": "MIT", 9 | "version": "0.1", 10 | "changelog": {} 11 | } 12 | -------------------------------------------------------------------------------- /views/dom/domdiff-view/DOMView.js: -------------------------------------------------------------------------------- 1 | class DOMView { 2 | constructor(){ 3 | this.renders = []; 4 | } 5 | 6 | render(suggestedDelay=10){ 7 | clearTimeout(this.renderTimer); 8 | this.renderTimer = setTimeout(()=>{ 9 | // Cleanup 10 | this.renders.forEach((view)=>{ 11 | view.destroy(); 12 | }); 13 | 14 | // Render new views 15 | document.querySelectorAll("dom-view-template").forEach(async (template)=>{ 16 | if (DOMView.DEBUG) console.log("Parsing",template); 17 | let root = new RootParseNode(template); 18 | if (DOMView.DEBUG) console.log("Rendering",template); 19 | this.renders.push(root.render()); 20 | if (DOMView.DEBUG) console.log("Ready for use"); 21 | }); 22 | },suggestedDelay); 23 | } 24 | 25 | existsAsViewElement(viewName){ 26 | return document.querySelector("dom-view-template [view='"+viewName+"']"); 27 | } 28 | 29 | /** 30 | * Gets the most specific binding with a value for a name in the scope 31 | * @param {type} bindingName 32 | * @param {type} scope 33 | * @returns {undefined|DOMView.getBindingFromScope.scope} 34 | */ 35 | static getBindingFromScope(bindingName, scope){ 36 | for (let i = scope.length - 1; i >= 0; i--) { 37 | if (scope[i].hasBindingFor(bindingName)) { 38 | return scope[i]; 39 | } 40 | } 41 | return undefined; 42 | } 43 | 44 | /** 45 | * Convenience method that evaluates a value in scope 46 | * @param {type} bindingName 47 | * @param {type} scope 48 | * @returns {undefined} 49 | */ 50 | async evaluateValueInScope(bindingName, scope) { 51 | let binding = DOMView.getBindingFromScope(bindingName, scope); 52 | if (binding===undefined) return undefined; 53 | 54 | return await binding.getValueFor(bindingName); 55 | } 56 | 57 | /** 58 | * Gets an ordered list from the scope mapped by the mapper function given 59 | */ 60 | getFilteredPath(viewElement, mapperFunction){ 61 | let element = viewElement; 62 | while (element != null && !element.scope){ 63 | element = element.parentElement; 64 | if (element==null){ 65 | // No concepts in this tree path at all 66 | return []; 67 | } 68 | } 69 | 70 | let result = []; 71 | if(element != null && element.viewParticle != null) { 72 | for (let binding of element.viewParticle.scope) { 73 | let mappedValue = mapperFunction(binding); 74 | if (mappedValue) result.push(mappedValue); 75 | } 76 | } 77 | return result; 78 | } 79 | 80 | /** 81 | * Gets an ordered list of concepts instances involved in rendering this view element 82 | * @param {HTMLElement} viewElement 83 | * @returns {string[]} 84 | */ 85 | getConceptPath(viewElement){ 86 | return this.getFilteredPath(viewElement, (binding)=>{ 87 | if (binding instanceof ConceptInstanceBinding) return binding; 88 | }); 89 | } 90 | 91 | /** 92 | * Gets an ordered list of template elements involved in rendering this view element 93 | * @param {HTMLElement} viewElement 94 | * @returns {string[]} 95 | */ 96 | getTemplatePath(viewElement){ 97 | return this.getFilteredPath(viewElement, (binding)=>{ 98 | if (binding instanceof TemplateBinding) return binding.templateElement; 99 | }); 100 | } 101 | 102 | /** 103 | * Gets an ordered list of properties involved in rendering this view element 104 | * @param {HTMLElement} viewElement 105 | * @returns {string[]} 106 | */ 107 | getPropertyPath(viewElement){ 108 | return this.getFilteredPath(viewElement, (binding)=>{ 109 | if (binding instanceof PropertyBinding) return {uuid: binding.uuid, property: binding.property}; 110 | }); 111 | } 112 | } 113 | 114 | DOMView.DEBUG = false; 115 | DOMView.DEBUG_PERFORMANCE = false; 116 | DOMView.singleton = new DOMView(); 117 | window.DOMView = DOMView; 118 | 119 | 120 | //If fragments exists postpone the DOMView until all fragments was loaded at least first time. (Fragments added later obviously does not count) 121 | if(typeof Fragment !== "undefined") { 122 | Fragment.addAllFragmentsLoadedCallback(()=>{ 123 | // We started after autoDOM has run, so no mutations. Bootstrap with what we have 124 | console.log("DOMDiffView: Full re-render due to initial page load via Codestrates"); 125 | DOMView.singleton.render(); 126 | 127 | // STUB: Reload when templates change 128 | let observer = new MutationObserver((mutations) => { 129 | let reload = function reloadDueToMutations(){ 130 | console.log("STUB: DOMDiffView: Full re-render due to templates changing"); 131 | DOMView.singleton.render(300); 132 | }; 133 | for(let mutation of mutations) { 134 | // From inside a template 135 | if ( 136 | (mutation.target.matches && mutation.target.matches("dom-view-template")) 137 | || (mutation.target.closest && mutation.target.closest("dom-view-template")) 138 | || (mutation.target.parentElement && mutation.target.parentElement.closest("dom-view-template"))){ 139 | reload(); 140 | break; 141 | } 142 | 143 | // From outside a template 144 | if (mutation.type==="childList"){ 145 | for (let node of [...mutation.addedNodes, ...mutation.removedNodes]){ 146 | if (node.tagName === "DOM-VIEW-TEMPLATE" || (node.querySelector && node.querySelector("dom-view-template"))){ 147 | reload(); 148 | break; 149 | } 150 | } 151 | } 152 | }; 153 | }); 154 | observer.observe(document.body, { 155 | attributes: true, 156 | attributeOldValue: false, 157 | childList: true, 158 | subtree: true, 159 | characterData: true, 160 | characterDataOldValue: false 161 | }); 162 | 163 | VarvEngine.registerEventCallback("engineReloaded", (evt) => { 164 | console.log("DOMDiffView: Full re-render due to engine reload"); 165 | DOMView.singleton.render(); 166 | }); 167 | }); 168 | } else { 169 | VarvEngine.registerEventCallback("engineReloaded", (evt) => { 170 | console.log("DOMDiffView: Full re-render due to engine reload"); 171 | DOMView.singleton.render(); 172 | }); 173 | } 174 | -------------------------------------------------------------------------------- /views/dom/domdiff-view/ParseNode.js: -------------------------------------------------------------------------------- 1 | class ParseNode { 2 | constructor(templateElement){ 3 | this.children = []; 4 | this.cleanupCallbacks = []; 5 | this.templateElement = templateElement; 6 | 7 | if (DOMView.DEBUG){ 8 | console.log("adding ", templateElement); 9 | } 10 | } 11 | 12 | parseTemplateNode(elementNode, parseOptions={}){ 13 | switch (elementNode.nodeType){ 14 | // Nodes that cannot have attributes are treated directly 15 | case Node.COMMENT_NODE: 16 | // Drop all comments to minify view as much as possible - we cannot update them properly anyways 17 | return null; 18 | case Node.TEXT_NODE: 19 | return new TextParseNode(elementNode); 20 | 21 | // Nodes that may have attributes are handled below 22 | case Node.ELEMENT_NODE: 23 | break; 24 | 25 | // Unknown nodes are copied verbatim 26 | default: 27 | return new YotaParseNode(elementNode); 28 | } 29 | 30 | // Handle filtering/duplication attributes before anything else (may need duplication) 31 | if (!parseOptions.skipQuery){ 32 | const atts = elementNode.attributes; 33 | if (atts && (atts["concept"] || atts["property"] || atts["if"])){ 34 | // These are not valid on varv-template element 35 | if (elementNode==="VARV-TEMPLATE") { 36 | console.log("concept, property or if used on varv-template element itself is invalid"); 37 | } 38 | return new QueryParseNode(elementNode); 39 | } 40 | } 41 | 42 | // Handle HTML elements 43 | switch (elementNode.tagName){ 44 | case "VARV-TEMPLATE": 45 | // Not used in output 46 | return null; 47 | case "TEMPLATE-REF": 48 | return new TemplateRefParseNode(elementNode); 49 | default: 50 | return new ElementParseNode(elementNode); 51 | } 52 | } 53 | 54 | getErrorView(targetDocument, scope, error, ex=null){ 55 | console.log("DOMView runtime error", error, ex, scope, this.templateElement, this); 56 | 57 | let element = targetDocument.createElement("varv-failure"); 58 | element.setAttribute("title", error + "\n" + ex); 59 | 60 | return new ViewParticle(element, this, scope); 61 | } 62 | } 63 | 64 | window.ParseNode = ParseNode; -------------------------------------------------------------------------------- /views/dom/domdiff-view/UpdatingEvalutation.js: -------------------------------------------------------------------------------- 1 | class UpdatingEvaluation { 2 | constructor(originalText, scope, onChangeCallback){ 3 | this.originalText = originalText; 4 | this.replacements = new Map(); 5 | this.tokens = originalText.match(/{(.+?)}/g); 6 | if (!this.tokens) this.tokens = []; 7 | this.onChangeCallback = onChangeCallback; 8 | this.updateCallbacks = []; 9 | this.destroyed = false; 10 | 11 | let self = this; 12 | 13 | // Prepare it once manually 14 | for(let token of this.tokens) { 15 | token = token.trim(); 16 | let lookupQuery = token.substring(1, token.length - 1); 17 | 18 | let replacement = { 19 | value: undefined, 20 | getText: null, 21 | binding: null, 22 | propertyName: null 23 | }; 24 | if (lookupQuery.includes("?")){ 25 | let regexp = /^(?.+?)\?(?["']?)(?.+?)\k(?::(?["']?)(?.*)\k)?$/gm; 26 | 27 | let match = regexp.exec(lookupQuery); 28 | 29 | replacement.propertyName = match.groups.condition; 30 | 31 | let negated = false; 32 | 33 | if(replacement.propertyName.startsWith("!")) { 34 | negated = true; 35 | replacement.propertyName = replacement.propertyName.substring(1); 36 | } 37 | 38 | // Fancy { x ? y : < } query 39 | binding = DOMView.getBindingFromScope(replacement.propertyName, scope); 40 | replacement.textFunction = ()=>{ 41 | if (replacement.binding===undefined) return undefined; 42 | let trueValue = match.groups.true; 43 | let falseValue = typeof match.groups.false === "undefined"?"":match.groups.false; 44 | 45 | if(negated) { 46 | let tmp = trueValue; 47 | trueValue = falseValue; 48 | falseValue = tmp; 49 | } 50 | 51 | return replacement.value?trueValue:falseValue; 52 | }; 53 | } else { 54 | // Normal {} query, the entire thing is the name 55 | replacement.propertyName = lookupQuery; 56 | replacement.binding = DOMView.getBindingFromScope(replacement.propertyName, scope); 57 | replacement.textFunction = ()=>{ 58 | return replacement.value; 59 | }; 60 | } 61 | this.replacements.set(lookupQuery, replacement); 62 | } 63 | 64 | this.initialUpdate = true; 65 | this.update(); 66 | } 67 | 68 | async update(){ 69 | let self = this; 70 | let mark = VarvPerformance.start(); 71 | 72 | // Get the initial values the first time 73 | if (this.initialUpdate){ 74 | await Promise.all(Array.from(this.replacements.values()).map(async (replacement)=>{ 75 | // Fetch initial value 76 | if (!replacement.binding) return; 77 | replacement.value = await replacement.binding.getValueFor(replacement.propertyName); 78 | 79 | // Listen for future updates, if supported by the binding 80 | if (replacement.binding.generateRawChangeListener){ 81 | let changedCallback = replacement.binding.generateRawChangeListener(replacement.propertyName, replacement.value); 82 | changedCallback.onChanged = async function updateUpdatingStringEvaluation(value){ 83 | replacement.value = value; 84 | await self.update(); 85 | }; 86 | this.updateCallbacks.push(changedCallback); 87 | }; 88 | })); 89 | 90 | this.initialUpdate = false; 91 | } 92 | 93 | try { 94 | let text = this.originalText; 95 | for(let token of this.tokens) { 96 | token = token.trim(); 97 | let lookupQuery = token.substring(1, token.length - 1); 98 | 99 | let value = this.replacements.get(lookupQuery).textFunction(); 100 | if (value !== undefined){ 101 | text = text.replace(token, value); // STUB: This can fail if the first token is replaced with something that looks like the second token 102 | } 103 | } 104 | 105 | await this.onChangeCallback(text); 106 | } catch (ex){ 107 | console.error(ex); 108 | } 109 | 110 | VarvPerformance.stop("UpdatingStringEvaluation.update", mark); 111 | } 112 | 113 | destroy(){ 114 | if (this.destroyed) { 115 | if (DOMView.DEBUG){ 116 | console.warn("FIXME: Harmless double desctruction, ignoring - but try not to destroy me this much"); 117 | } 118 | return; 119 | } 120 | for (let entry of this.updateCallbacks){ 121 | entry.destroy(); 122 | } 123 | this.destroyed = true; 124 | } 125 | } 126 | 127 | window.UpdatingEvaluation = UpdatingEvaluation; -------------------------------------------------------------------------------- /views/dom/domdiff-view/ViewParticle.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A view particle mst 3 | * @type type 4 | */ 5 | class ViewParticle { 6 | constructor(node, parseNode, scope){ 7 | this.node = node; 8 | this.parseNode = parseNode; 9 | this.mountCallbacks = []; 10 | this.renderCallbacks = []; 11 | this.cleanup = []; 12 | this.scope = scope; 13 | this.initialRender = false; 14 | node.viewParticle = this; 15 | } 16 | 17 | getNode(){ 18 | // TODO: error node when null 19 | return this.node; 20 | } 21 | 22 | getTargetDocument(){ 23 | let doc = this.node.ownerDocument; 24 | if (!doc.createElement) console.log("Weird root node", this, this.node, doc, this.parseNode, this.scope); 25 | return doc; 26 | } 27 | 28 | addCleanup(callback){ 29 | this.cleanup.push(callback); 30 | } 31 | 32 | addOnMountedCallback(callback){ 33 | this.mountCallbacks.push(callback); 34 | } 35 | 36 | addOnRenderedCallback(callback){ 37 | this.renderCallbacks.push(callback); 38 | } 39 | 40 | mountInto(parentElement, insertBeforeNode=null){ 41 | parentElement.insertBefore(this.node, insertBeforeNode); 42 | this.mountCallbacks.forEach((callback)=>{ 43 | callback(); 44 | }); 45 | } 46 | 47 | onRendered(){ 48 | this.initialRender = true; 49 | this.renderCallbacks.forEach((callback)=>{ 50 | callback(); 51 | }); 52 | } 53 | 54 | hasRendered(){ 55 | return this.initialRender; 56 | } 57 | 58 | destroy(){ 59 | if (DOMView.DEBUG) console.log("Destroying particle ",this); 60 | this.cleanup.forEach((callback)=>{ 61 | callback(); 62 | }); 63 | this.node.remove(); 64 | } 65 | } 66 | 67 | window.ViewParticle = ViewParticle; -------------------------------------------------------------------------------- /views/dom/domdiff-view/bindings/ConceptInstanceBinding.js: -------------------------------------------------------------------------------- 1 | class ConceptInstanceBinding { 2 | constructor(concept, uuid) { 3 | if (!uuid) throw new Error("Invalid reference to concept instance with a null or undefined uuid '"+uuid+"' and concept '"+concept+"'"); 4 | if (!concept) throw new Error("Invalid reference to unknown concept with uuid '"+uuid+"', concept is "+concept); 5 | this.concept = concept; 6 | this.uuid = uuid; 7 | } 8 | 9 | hasBindingFor(name) { 10 | try { 11 | this.getProperty(name); 12 | return true; 13 | } catch (ex) { 14 | return false; 15 | } 16 | } 17 | 18 | getProperty(lookupName){ 19 | if(lookupName.startsWith(this.concept.name+".")) { 20 | lookupName = lookupName.substring(this.concept.name.length+1); 21 | } 22 | return this.concept.getProperty(lookupName); 23 | } 24 | 25 | generateRawChangeListener(lookupName, initialValue=null){ 26 | let self = this; 27 | let oldValue; 28 | if (Array.isArray(initialValue)){ 29 | oldValue = initialValue.slice(); 30 | } else { 31 | oldValue = initialValue; 32 | } 33 | 34 | let property = this.getProperty(lookupName); 35 | 36 | let result = { 37 | onChanged: async ()=>{console.error("DOMView bug: ConceptInstanceBinding raw change listener called without anything hooked up to it", self.concept, self.uuid, self);} 38 | }; 39 | 40 | // Listen for changes in the looked up property 41 | let changedCallback = async function queryParseNodePropertyChanged(uuid, value){ 42 | if (uuid===self.uuid){ 43 | let identical = false; 44 | if (Array.isArray(value)){ 45 | identical = ScopedParseNode.fastDeepEqual(value, oldValue); 46 | } else { 47 | identical = (oldValue===value); 48 | } 49 | 50 | if (!identical){ 51 | if (Array.isArray(value)){ 52 | oldValue = value.slice(); 53 | } else { 54 | oldValue = value; 55 | } 56 | await result.onChanged(value); 57 | } 58 | } 59 | }; 60 | property.addUpdatedCallback(changedCallback); 61 | result.destroy = ()=>{ 62 | property.removeUpdatedCallback(changedCallback); 63 | }; 64 | return result; 65 | } 66 | 67 | async getValueFor(name) { 68 | let property = null; 69 | try { 70 | property = this.getProperty(name); 71 | } catch(e) { 72 | //Ignore 73 | } 74 | 75 | if(property === null) { 76 | return undefined; 77 | } 78 | 79 | return await property.getValue(this.uuid); 80 | } 81 | 82 | async setValueFor(name, value){ 83 | const property = this.concept.getProperty(name); 84 | await property.setValue(this.uuid, property.typeCast(value)); 85 | } 86 | } 87 | 88 | window.ConceptInstanceBinding = ConceptInstanceBinding; -------------------------------------------------------------------------------- /views/dom/domdiff-view/bindings/PropertyArrayEntryBinding.js: -------------------------------------------------------------------------------- 1 | class PropertyArrayEntryBinding extends PropertyBinding { 2 | constructor(property, uuid, boundValue, index, length, as=null) { 3 | super(property, uuid, boundValue, as); 4 | this.index = index; 5 | this.length = length; 6 | this._reIndexCount = 0; 7 | this._indexCallbacks = []; 8 | } 9 | 10 | hasBindingFor(name){ 11 | // We don't supply this ourselves 12 | if (super.hasBindingFor(name)){ 13 | return true; 14 | } 15 | 16 | if (name===".index") return true; 17 | if (name===".size") return true; 18 | if (name==="view::reused") return true; 19 | if (this.as){ 20 | // We are available under as.value 21 | return name===this.as+".index"; 22 | } else { 23 | return name===this.property.name+".index"; 24 | } 25 | } 26 | 27 | getValueFor(name){ 28 | if (!this.hasBindingFor(name)) throw new Error("PropertyArrayEntryBinding asked for value for "+name+" but does not support it"); 29 | if (name==="view::reused") return this._reIndexCount; 30 | 31 | if (name.endsWith(".size")){ 32 | return this.length; 33 | } else if (name.endsWith(".index")){ 34 | return this.index; 35 | } else { 36 | return this.boundValue; 37 | } 38 | } 39 | 40 | updateIndex(newIndex){ 41 | this._reIndexCount++; 42 | this.index = newIndex; 43 | this._indexCallbacks.forEach((callback)=>{ 44 | try { 45 | callback(); 46 | } catch (ex){ 47 | console.log(ex); 48 | } 49 | }); 50 | } 51 | 52 | addIndexCallback(callback) { 53 | this._indexCallbacks.push(callback); 54 | } 55 | 56 | removeIndexCallback(callback) { 57 | let index = this._indexCallbacks.indexOf(callback); 58 | if (index===-1){ 59 | console.warn("Cannot remove indexcallback that isn't part of list of callbacks: "+callback+" list is "+this._indexCallbacks); 60 | return; 61 | } 62 | this._indexCallbacks.splice(index, 1); 63 | } 64 | 65 | generateRawChangeListener(name, oldValue=null){ 66 | if (!this.hasBindingFor(name)) throw new Error("PropertyArrayEntryBinding asked for change listener for "+name+" but does not support it"); 67 | let self = this; 68 | 69 | let result = { 70 | onChanged: async ()=>{console.error("DOMView bug: PropertyArrayEntryBinding raw change listener called without anything hooked up to it");}, 71 | destroy: ()=>{} 72 | }; 73 | let changedCallback = false; 74 | if (name==="view::reused"){ 75 | changedCallback = async function indexChanged(){ 76 | await result.onChanged(self._reIndexCount); 77 | }; 78 | } else if (name.endsWith(".index")){ 79 | changedCallback = async function indexChanged(){ 80 | await result.onChanged(self.index); 81 | }; 82 | } 83 | 84 | if (changedCallback){ 85 | self.addIndexCallback(changedCallback); 86 | result.destroy = ()=>{ 87 | self.removeIndexCallback(changedCallback); 88 | }; 89 | } 90 | return result; 91 | } 92 | 93 | identicalExceptIndex(otherBinding){ 94 | return this.property === otherBinding.property && 95 | this.uuid === otherBinding.uuid && 96 | this.boundValue === otherBinding.boundValue; 97 | } 98 | } 99 | 100 | window.PropertyArrayEntryBinding = PropertyArrayEntryBinding; -------------------------------------------------------------------------------- /views/dom/domdiff-view/bindings/PropertyBinding.js: -------------------------------------------------------------------------------- 1 | class PropertyBinding { 2 | constructor(property, uuid, boundValue, as=null) { 3 | this.uuid = uuid; 4 | this.property = property; 5 | this.boundValue = boundValue; 6 | this.as = as; 7 | } 8 | 9 | hasBindingFor(name){ 10 | if (name===".value") return true; 11 | if (this.as){ 12 | // We are available under as.value 13 | return name===this.as+".value"; 14 | } else { 15 | return name===this.property.name+".value"; 16 | } 17 | 18 | return false; 19 | } 20 | 21 | getValueFor(name){ 22 | if (!this.hasBindingFor(name)) throw new Error("PropertyBinding asked for value for "+name+" but only supports "+this.property.name+".value/"+this.as+".value"); 23 | return this.boundValue; 24 | } 25 | } 26 | 27 | window.PropertyBinding = PropertyBinding; -------------------------------------------------------------------------------- /views/dom/domdiff-view/bindings/RuntimeExceptionBinding.js: -------------------------------------------------------------------------------- 1 | class RuntimeExceptionBinding { 2 | constructor(errorMessage, ex) { 3 | this.errorMessage = errorMessage; 4 | this.ex = ex; 5 | } 6 | 7 | hasBindingFor(name) { 8 | return false; 9 | } 10 | 11 | getValueFor(name) { 12 | throw new Error("Unsupported operation"); 13 | } 14 | } 15 | 16 | window.RuntimeExceptionBinding = RuntimeExceptionBinding; -------------------------------------------------------------------------------- /views/dom/domdiff-view/bindings/TemplateBinding.js: -------------------------------------------------------------------------------- 1 | class TemplateBinding { 2 | constructor(templateRootElement) { 3 | this.templateElement = templateRootElement; 4 | } 5 | 6 | getTemplateElement(){ 7 | return this.templateElement; 8 | } 9 | 10 | hasBindingFor(name) { 11 | return false; 12 | } 13 | } 14 | 15 | window.TemplateBinding = TemplateBinding; -------------------------------------------------------------------------------- /views/dom/domdiff-view/bindings/ValueBinding.js: -------------------------------------------------------------------------------- 1 | class ValueBinding { 2 | constructor(bindings) { 3 | this.bindings = bindings; 4 | } 5 | 6 | hasBindingFor(name) { 7 | return this.bindings.hasOwnProperty(name); 8 | } 9 | 10 | getValueFor(name) { 11 | return this.bindings[name]; 12 | } 13 | } 14 | 15 | window.ValueBinding = ValueBinding; -------------------------------------------------------------------------------- /views/dom/domdiff-view/descriptor.json: -------------------------------------------------------------------------------- 1 | { 2 | "friendlyName": "Varv DOMDiff View", 3 | "description": "Allow data to be visualized as a high-performance browser-based UI based on DOM-diffing", 4 | "dependencies": [ 5 | "#varv-engine" 6 | ], 7 | "assets": [], 8 | "license": "MIT", 9 | "version": "0.1", 10 | "changelog": {} 11 | } 12 | -------------------------------------------------------------------------------- /views/dom/domdiff-view/parsetree/ElementParseNode.js: -------------------------------------------------------------------------------- 1 | class ElementParseNode extends ParseNode { 2 | constructor(templateElement){ 3 | super(templateElement); 4 | 5 | // Parse the children 6 | for (let childNode of templateElement.childNodes){ 7 | let parseChild = this.parseTemplateNode(childNode); 8 | if (parseChild){ 9 | // If this actually needs parsing, add it 10 | this.children.push(parseChild); 11 | } 12 | } 13 | } 14 | 15 | getView(targetDocument, scope){ 16 | let self = this; 17 | if (DOMView.DEBUG) console.log("instantiating element", this.templateElement); 18 | 19 | // Create the element itself 20 | let name = this.templateElement.tagName; 21 | let namespace = this.templateElement.namespaceURI; 22 | if (name==="DOM-VIEW-TEMPLATE") name = "varv-view"; // The template itself renames to view 23 | let element; 24 | if (namespace.endsWith("html")){ 25 | element = targetDocument.createElement(name); 26 | } else { 27 | element = targetDocument.createElementNS(namespace, name); 28 | } 29 | let view = new ViewParticle(element, this, scope); 30 | element.scope = scope; 31 | element.templateElement = this.templateElement; 32 | element.parseNode = this; 33 | 34 | // Attach the children 35 | view.childViews = []; 36 | for (let child of this.children){ 37 | let childView = child.getView(targetDocument, scope); 38 | view.childViews.push(childView); 39 | childView.mountInto(element); 40 | } 41 | 42 | // Evaluate all attributes 43 | for(let attr of Array.from(this.templateElement.attributes)) { 44 | // Filtering attributes are ignored 45 | if (attr.name==="concept" || attr.name==="property" || attr.name==="if") continue; 46 | 47 | // Selects are a bit special, they depend on their children being present in order to set value 48 | if (self.templateElement.tagName==="SELECT" && attr.name==="value"){ 49 | let selectUpdater = function selectChildrenOptionsChanged(){ 50 | element.value = element.varvValue; 51 | }; 52 | view.childViews.forEach((childView)=>{ 53 | childView.addOnRenderedCallback(()=>{ 54 | setTimeout(selectUpdater,0); 55 | }); 56 | }); 57 | } 58 | 59 | // The rest are evaluated 60 | let updatingEvaluation = new UpdatingEvaluation(attr.value, scope, function attributeNodeUpdated(value){ 61 | let shouldUpdateAttribute = true; 62 | 63 | // Check for special attributes 64 | if (attr.name==="value"){ 65 | switch (element.tagName){ 66 | case "INPUT": 67 | shouldUpdateAttribute = false; 68 | if (self.templateElement.type==="checkbox"){ 69 | element.checked = value==="true" || value===true; 70 | } else { 71 | element.value = value; 72 | } 73 | break; 74 | case "TEXTAREA": 75 | shouldUpdateAttribute = false; 76 | element.value = value; 77 | break; 78 | case "SELECT": 79 | // Update when children option render properly 80 | shouldUpdateAttribute = false; 81 | element.value = value; 82 | element.varvValue = value; 83 | break; 84 | case "DIV": 85 | shouldUpdateAttribute = false; 86 | if (!element.blockReadbacks){ 87 | element.innerHTML = value; 88 | } 89 | break; 90 | } 91 | } else if(attr.name === "disabled" && value === "false") { 92 | // Don't move disabled=false over 93 | shouldUpdateAttribute = false; 94 | element.removeAttribute(attr.name); 95 | } 96 | if (shouldUpdateAttribute){ 97 | element.setAttribute(attr.name, value); 98 | } 99 | }); 100 | view.addCleanup(()=>{ 101 | updatingEvaluation.destroy(); 102 | }); 103 | } 104 | this.addAttributeWriteBacks(element, scope); 105 | 106 | // Also kill all the children if we get killed 107 | view.addCleanup(()=>{ 108 | view.childViews.forEach((childView)=>{ 109 | childView.destroy(); 110 | }); 111 | }); 112 | return view; 113 | } 114 | 115 | addAttributeWriteBacks(element, scope){ 116 | // Check for special elements that can push data back to the concepts 117 | if (element.tagName==="INPUT" || element.tagName==="TEXTAREA"){ 118 | let writeBack = this.getAttributeWriteBack(this.templateElement.getAttribute("value"), scope); 119 | if (writeBack!==null){ 120 | element.addEventListener("input", ()=>{ 121 | writeBack.set(element.getAttribute("type")==="checkbox"?element.checked:element.value); 122 | }); 123 | } 124 | } else if (element.tagName==="SELECT"){ 125 | let writeBack = this.getAttributeWriteBack(this.templateElement.getAttribute("value"), scope); 126 | if (writeBack!==null){ 127 | element.addEventListener("input", ()=>{ 128 | writeBack.set(element.value); 129 | }); 130 | } 131 | } else if (element.tagName==="DIV" && element.getAttribute("contenteditable")!==null){ 132 | let writeBack = this.getAttributeWriteBack(this.templateElement.getAttribute("value"), scope); 133 | if (writeBack!==null){ 134 | let coalesceTimer = null; 135 | let needsAnotherUpdate = false; 136 | element.addEventListener("input", async ()=>{ 137 | needsAnotherUpdate = true; 138 | if (!coalesceTimer){ 139 | coalesceTimer = setTimeout(async ()=>{ 140 | while (needsAnotherUpdate){ 141 | needsAnotherUpdate = false; 142 | element.blockReadbacks = true; // Avoid reading our own changes back 143 | await writeBack.set(element.innerHTML); 144 | element.blockReadbacks = false; 145 | } 146 | coalesceTimer = null; 147 | }, 100); 148 | } 149 | }); 150 | } 151 | } 152 | } 153 | 154 | 155 | /** 156 | * Some attributes, like input.value, can bind to simple writebacks "{someProperty}" that are looked up in scope. 157 | * @returns {binding} A binding if it exists in scope or null 158 | */ 159 | getAttributeWriteBack(rawAttributeValue, scope){ 160 | if (rawAttributeValue===undefined || rawAttributeValue===null) return null; // Not set 161 | if (!(rawAttributeValue.startsWith("{") && rawAttributeValue.endsWith("}"))) return null; // Not a dynamic binding at all 162 | 163 | let valueLookupName = rawAttributeValue.substring(1, rawAttributeValue.length - 1); 164 | let binding = DOMView.getBindingFromScope(valueLookupName,scope); 165 | if (!(binding instanceof ConceptInstanceBinding)){ 166 | console.warn("DOMDiffView: Writeback attribute resolves to something that is not a writable property on a concept, assuming read-only", rawAttributeValue); 167 | return null; 168 | } else { 169 | return { 170 | set: async function attributeWriteBack(value){ 171 | await binding.setValueFor(valueLookupName, value); 172 | } 173 | }; 174 | } 175 | } 176 | 177 | } 178 | 179 | window.ElementParseNode = ElementParseNode; -------------------------------------------------------------------------------- /views/dom/domdiff-view/parsetree/RootParseNode.js: -------------------------------------------------------------------------------- 1 | class RootParseNode extends ElementParseNode { 2 | render(){ 3 | // Find the target document 4 | let targetFrameSpec = this.templateElement.getAttribute("target-iframe"); 5 | let targetDocument; 6 | if (targetFrameSpec){ 7 | let frame = document.querySelector(targetFrameSpec); 8 | if (!frame) throw new Error("DOMDiffView: dom-view-template with target-iframe that does not exist in document failed to render", targetFrameSpec, templateElement); 9 | targetDocument = frame.contentDocument; 10 | } else { 11 | targetDocument = document; 12 | } 13 | 14 | // Construct the view 15 | let scope = [new TemplateBinding(this.templateElement)]; 16 | let view = this.getView(targetDocument, scope); 17 | 18 | // Find the target element 19 | let targetSpec = this.templateElement.getAttribute("target-element"); 20 | if (targetFrameSpec){ 21 | // Rendering to an iframe 22 | if (targetSpec){ 23 | // This template uses a custom render target element, try to find it 24 | let targetElement = targetDocument.querySelector(targetSpec); 25 | if (targetElement){ 26 | view.mountInto(targetElement); 27 | } else { 28 | console.error("DOMView: Rendering into nothingness since template target-element does not exist in target iframe: ", targetFrameSpec, targetSpec); 29 | } 30 | } else { 31 | // Just plain add it to body 32 | view.mountInto(targetDocument.body); 33 | } 34 | } else { 35 | // Rendering to the local document 36 | if (targetSpec){ 37 | // This template uses a custom render target element, try to find it 38 | let targetElement = targetDocument.querySelector(targetSpec); 39 | if (targetElement){ 40 | view.mountInto(targetElement); 41 | } else { 42 | console.error("DOMView: Rendering into nothingness since template target-element does not exist in document: ", targetSpec); 43 | } 44 | } else { 45 | // Default is to render just after the template element. 46 | // Special-case for CodeStrates-based templates (avoid getting deleted inside autoDOM) 47 | let autoDOM = this.templateElement.closest(".autoDom"); 48 | if (autoDOM){ 49 | // Add after autoDOM instead of inside of it 50 | if (!autoDOM.parentNode){ 51 | console.log("DOMView: Was rendering an autoDOM template but it had no parent", templateElement); 52 | } 53 | view.mountInto(autoDOM.parentNode,autoDOM.nextElementSibling); 54 | } else { 55 | // Outside we just insert after the template directly 56 | if (!this.templateElement.parentNode){ 57 | console.log("DOMView: Was rendering a non autoDOM template but it had no parent", templateElement); 58 | } 59 | view.mountInto(this.templateElement.parentNode, this.templateElement.nextElementSibling); 60 | } 61 | } 62 | } 63 | 64 | return view; 65 | } 66 | }; 67 | 68 | window.RootParseNode = RootParseNode; -------------------------------------------------------------------------------- /views/dom/domdiff-view/parsetree/ScopedParseNode.js: -------------------------------------------------------------------------------- 1 | class ScopedParseNode extends ParseNode { 2 | constructor(templateElement){ 3 | super(templateElement); 4 | } 5 | 6 | getView(targetDocument, scope){ 7 | if (DOMView.DEBUG) console.log("instantiating scopedparsenode abstract view for ", this.templateElement); 8 | let self = this; 9 | let view = new ViewParticle(targetDocument.createProcessingInstruction("varv-scope-anchor", {}), this, scope); 10 | view.topGuardElement = view.getTargetDocument().createProcessingInstruction("varv-scope-topguard", {}); 11 | view.childViews = []; 12 | this.generateScopes(view); 13 | 14 | view.addOnMountedCallback(()=>{ 15 | // Plain move everything to new parent 16 | view.getNode().parentElement.insertBefore(view.topGuardElement, view.getNode()); 17 | view.childViews.forEach((childView)=>{ 18 | // Insert them before our anchor node 19 | childView.mountInto(view.getNode().parentElement, view.getNode()); 20 | }); 21 | }); 22 | view.addCleanup(()=>{ 23 | // Empty the view 24 | self.onScopesUpdated(view, []); 25 | }); 26 | return view; 27 | } 28 | 29 | onScopesUpdated(view, newChildScopes){ 30 | // Destroy views that are no longer in the new child scopes 31 | let self = this; 32 | let changes = 0; 33 | 34 | // Add new views for newly added scopes while reordering 35 | if (DOMView.DEBUG) console.log("Updating view scope, old=>new",view.childViews, newChildScopes); 36 | let oldChildViews = view.childViews; 37 | view.childViews = []; 38 | newChildScopes.forEach((newChildScope)=>{ 39 | let existingView = false; 40 | oldChildViews.forEach((childView)=>{ 41 | if (ScopedParseNode.fastDeepEqual(childView.localScope,newChildScope)) existingView = childView; 42 | }); 43 | if (existingView){ 44 | oldChildViews.splice(oldChildViews.indexOf(existingView),1); 45 | view.childViews.push(existingView); 46 | } else { 47 | view.childViews.push(()=>{ 48 | /** 49 | * Very specific performance optimization: 50 | * TODO: If the only change in the scope is a PropertyArrayEntryBinding index on the top of the scope stack 51 | * we can migrate the view by performing binding updates instead of destroying it and recreating it here. 52 | * This happens often when reordering list entries. 53 | */ 54 | let newLastOfScope = newChildScope[newChildScope.length-1]; 55 | if (newLastOfScope instanceof PropertyArrayEntryBinding){ 56 | for (let oldChildView of oldChildViews){ 57 | let oldLastOfScope = oldChildView.localScope[oldChildView.localScope.length-1]; 58 | if (oldLastOfScope instanceof PropertyArrayEntryBinding){ 59 | if (oldLastOfScope.identicalExceptIndex(newLastOfScope)){ 60 | // Identical tops (except index), compare rest of their localScope 61 | if (DOMView.DEBUG || DOMView.DEBUG_PERFORMANCE) console.log("Could optimize maybe", newChildScope, oldChildView.localScope); 62 | if (ScopedParseNode.fastDeepEqual(newChildScope.slice(0,-1),oldChildView.localScope.slice(0,-1))){ 63 | if (DOMView.DEBUG || DOMView.DEBUG_PERFORMANCE) console.log("Optimized",oldChildView); 64 | oldChildViews.splice(oldChildViews.indexOf(oldChildView),1); 65 | oldLastOfScope.updateIndex(newLastOfScope.index); 66 | return oldChildView; 67 | } 68 | } 69 | } 70 | }; 71 | } 72 | // /very specific performance optimization 73 | 74 | // Couldn't recover anything, just create a new view 75 | let childView; 76 | if (newLastOfScope instanceof RuntimeExceptionBinding){ 77 | childView = self.getErrorView(view.getTargetDocument(), [...view.scope, ...newChildScope], newLastOfScope.errorMessage, newLastOfScope.ex); 78 | } else { 79 | childView = self.children[0].getView(view.getTargetDocument(),[...view.scope, ...newChildScope]); 80 | } 81 | changes++; 82 | childView.localScope = newChildScope; 83 | return childView; 84 | }); 85 | } 86 | }); 87 | 88 | // Run optimizing functions 89 | view.childViews = view.childViews.map((value)=>{ 90 | if (typeof(value)==="function") return value(); 91 | return value; 92 | }); 93 | 94 | for (let i = oldChildViews.length-1; i>=0; i--){ 95 | let found = false; 96 | view.childViews.forEach((childView)=>{ 97 | if (childView===oldChildViews[i]) found = true; 98 | }); 99 | if (!found){ 100 | oldChildViews[i].destroy(); 101 | oldChildViews.splice(i,1); 102 | changes++; 103 | } 104 | } 105 | if (oldChildViews.length!==0) console.log("FIXME: DOMView oldChildViews postcondition inconsistency detected: 0!="+oldChildViews.length,oldChildViews); 106 | 107 | if ((DOMView.DEBUG||DOMView.DEBUG_PERFORMANCE) && changes===0 && newChildScopes.length>0){ 108 | try { 109 | console.log("FIXME: DOMDiffView: Potential performance optimization for ScopedParseNode. onScopesUpdated() called but returned no change in scope",this); 110 | throw new Error("stacktrace"); 111 | } catch (ex) { 112 | console.log(ex); 113 | } 114 | } 115 | 116 | this.stubResetView(view); 117 | } 118 | 119 | stubResetView(view){ 120 | if (view.getNode().parentNode === null) return; // Not in any document yet 121 | 122 | let parent = view.getNode().parentNode; 123 | let viewCount = view.childViews.length; 124 | 125 | for (let i = 0; i < viewCount; i++){ 126 | if (DOMView.DEBUG) console.log("Validating view", view.childViews[i]); 127 | let alreadyMountedCorrectly = true; 128 | 129 | // A view is mounted correctly if it is mounted here and no view that is supposed to be later is mounted before it 130 | if (view.childViews[i].getNode().parentElement!==parent){ 131 | alreadyMountedCorrectly = false; 132 | } 133 | let anchorIndex = [...parent.childNodes].indexOf(view.childViews[i].getNode()); 134 | if (i!==viewCount-1){ 135 | for (let o = i+1; o < viewCount; o++){ 136 | let thisIndex = [...parent.childNodes].indexOf(view.childViews[o].getNode()); 137 | if (thisIndex < anchorIndex && view.childViews[o].getNode().parentElement===parent) { 138 | console.log("Wrong location, found",anchorIndex, thisIndex,view.childViews[o].getNode()); 139 | 140 | alreadyMountedCorrectly = false; 141 | break; 142 | } 143 | } 144 | } 145 | 146 | // If not mounted correctly, mount after previous view (or top guard if first view) 147 | if (!alreadyMountedCorrectly){ 148 | let mountAfter = view.topGuardElement; 149 | if (i!==0){ 150 | mountAfter = view.childViews[i-1].getNode(); 151 | } 152 | view.childViews[i].mountInto(view.getNode().parentElement, mountAfter.nextSibling); 153 | } 154 | } 155 | 156 | view.onRendered(); 157 | } 158 | 159 | showError(view, message, ex){ 160 | console.log(ex); 161 | this.onScopesUpdated(view, []); 162 | view.childViews.push(this.getErrorView(view.getTargetDocument(), view.scope, message, ex)); 163 | this.stubResetView(view); 164 | } 165 | 166 | static fastDeepEqual(a,b){ 167 | if (a === b) return true; 168 | 169 | if (a && b && typeof a == 'object' && typeof b == 'object') { 170 | if (a.constructor !== b.constructor) return false; 171 | 172 | var length, i, keys; 173 | if (Array.isArray(a)) { 174 | length = a.length; 175 | if (length != b.length) return false; 176 | for (i = length; i-- !== 0;) 177 | if (!ScopedParseNode.fastDeepEqual(a[i], b[i])) return false; 178 | return true; 179 | } 180 | 181 | if (a.constructor === RegExp) return a.source === b.source && a.flags === b.flags; 182 | if (a.valueOf !== Object.prototype.valueOf) return a.valueOf() === b.valueOf(); 183 | if (a.toString !== Object.prototype.toString) return a.toString() === b.toString(); 184 | 185 | keys = Object.keys(a); 186 | length = keys.length; 187 | if (length !== Object.keys(b).length) return false; 188 | 189 | for (i = length; i-- !== 0;) 190 | if (!Object.prototype.hasOwnProperty.call(b, keys[i])) return false; 191 | 192 | for (i = length; i-- !== 0;) { 193 | var key = keys[i]; 194 | if (key.startsWith("_")) continue; 195 | if (!ScopedParseNode.fastDeepEqual(a[key], b[key])) return false; 196 | } 197 | 198 | return true; 199 | } 200 | 201 | if(typeof a === "function" && typeof b === "function") { 202 | return true; 203 | } 204 | 205 | // true if both NaN, false otherwise 206 | return a!==a && b!==b; 207 | 208 | }; 209 | }; 210 | 211 | window.ScopedParseNode = ScopedParseNode; -------------------------------------------------------------------------------- /views/dom/domdiff-view/parsetree/TemplateRefParseNode.js: -------------------------------------------------------------------------------- 1 | class TemplateRefParseNode extends ScopedParseNode { 2 | constructor(templateElement){ 3 | super(templateElement); 4 | this.children.push(new TemplateInstanceParseNode(templateElement)); 5 | } 6 | 7 | getView(targetDocument, scope){ 8 | if (DOMView.DEBUG) console.log("instantiating template-ref for ", this.templateElement); 9 | return super.getView(targetDocument, scope); 10 | } 11 | 12 | // Look at the TEMPLATE-NAME attribute and generate our scope(s) 13 | generateScopes(view){ 14 | let self = this; 15 | let templateQuery = this.templateElement.getAttribute("template-name"); 16 | let templateHookType = this.templateElement.getAttribute("template-hook"); 17 | if ((templateQuery!==null) && templateQuery.trim().length>0){ 18 | // Need to monitor a list of templates 19 | view.templateUpdatingEvaluation = new UpdatingEvaluation(templateQuery, view.scope, async function templateNameAttributeChanged(templateName){ 20 | try { 21 | // Find the template and create corresponding scopes 22 | let templates = document.querySelectorAll("varv-template[name='" + templateName+"']"); 23 | let localScopes = []; 24 | 25 | if (templates.length===0) throw new Error("Template with name '"+templateName+"' does not exist (yet?)"); 26 | switch (templateHookType){ 27 | case "all": 28 | templates.forEach((template)=>{ 29 | localScopes.push([new TemplateBinding(template)]); 30 | }); 31 | break; 32 | case "first": 33 | localScopes.push([new TemplateBinding(templates[0])]); 34 | break; 35 | case "last": 36 | default: 37 | localScopes.push([new TemplateBinding(templates[templates.length - 1])]); 38 | } 39 | 40 | self.onScopesUpdated(view, localScopes); 41 | } catch (ex){ 42 | self.showError(view, "Template-ref='"+templateQuery+"': "+ex, ex); 43 | return; 44 | } 45 | }); 46 | view.addCleanup(()=>{ 47 | view.templateUpdatingEvaluation.destroy(); 48 | }); 49 | } 50 | } 51 | } 52 | window.TemplateRefParseNode = TemplateRefParseNode; 53 | 54 | /** 55 | * A node that dynamically parses and renders a template from the scope all during getView rather than construction 56 | * @type type 57 | */ 58 | class TemplateInstanceParseNode extends ParseNode { 59 | getView(targetDocument, scope){ 60 | let templateBinding = scope[scope.length-1]; 61 | if (!templateBinding instanceof TemplateBinding) throw new Error("STUB: Currently TemplateBinding MUST be the last element on the scope stack when rendering template instances"); 62 | 63 | // Dynamically parse and render the template now 64 | let referencedTemplateElement = templateBinding.getTemplateElement() 65 | if (DOMView.DEBUG) console.log("parsing template-instance for ", this.templateElement, referencedTemplateElement); 66 | let view = new ViewParticle(targetDocument.createProcessingInstruction("varv-template-anchor", {}), this, scope); 67 | let parseNodes = []; 68 | for (let childNode of referencedTemplateElement.childNodes){ 69 | let parseChild = this.parseTemplateNode(childNode); 70 | if (parseChild){ 71 | // If this actually needs parsing, add it 72 | parseNodes.push(parseChild); 73 | } 74 | } 75 | 76 | if (DOMView.DEBUG) console.log("creating template-instance view for ", view, parseNodes); 77 | view.childViews = []; 78 | parseNodes.forEach((parseChild)=>{ 79 | view.childViews.push(parseChild.getView(targetDocument, scope)); 80 | }); 81 | 82 | // Also destroy the template view children when our view is destroyed 83 | view.addCleanup(()=>{ 84 | view.childViews.forEach((childView)=>{ 85 | childView.destroy(); 86 | }); 87 | }); 88 | 89 | // When we are mounted into the document, think about the children! 90 | view.addOnMountedCallback(()=>{ 91 | view.childViews.forEach((childView)=>{ 92 | // Insert them before our anchor node 93 | childView.mountInto(view.getNode().parentNode, view.getNode()); 94 | }); 95 | }); 96 | 97 | return view; 98 | } 99 | }; 100 | window.TemplateInstanceParseNode = TemplateInstanceParseNode; -------------------------------------------------------------------------------- /views/dom/domdiff-view/parsetree/TextParseNode.js: -------------------------------------------------------------------------------- 1 | class TextParseNode extends ParseNode { 2 | getView(targetDocument, scope){ 3 | if (DOMView.DEBUG) console.log("instantiating text", this.templateElement); 4 | 5 | let textNode = targetDocument.createTextNode(""); 6 | let view = new ViewParticle(textNode, this, scope); 7 | view.updatingEvaluation = new UpdatingEvaluation(this.templateElement.nodeValue, scope, function textNodeUpdated(text){ 8 | textNode.nodeValue = text; 9 | }); 10 | view.addCleanup(()=>{ 11 | view.updatingEvaluation.destroy(); 12 | }); 13 | 14 | return view; 15 | } 16 | }; 17 | 18 | window.TextParseNode = TextParseNode; -------------------------------------------------------------------------------- /views/dom/domdiff-view/parsetree/YotaParseNode.js: -------------------------------------------------------------------------------- 1 | class YotaParseNode extends ParseNode { 2 | getView(targetDocument, scope){ 3 | if (DOMView.DEBUG) console.log("instantiating yota", this.templateElement); 4 | 5 | return new ViewParticle(targetDocument.importNode(this.templateElement,false), this, scope); 6 | } 7 | }; 8 | 9 | window.YotaParseNode = YotaParseNode; -------------------------------------------------------------------------------- /views/dom/domtriggers/descriptor.json: -------------------------------------------------------------------------------- 1 | { 2 | "friendlyName": "Varv engine triggers for DOM elements", 3 | "description": "A set of event handlers for triggering on DOM elements", 4 | "dependencies": [ 5 | 6 | ], 7 | "optionalDependencies": [ 8 | "#varv-engine" 9 | ], 10 | "assets": [], 11 | "license": "MIT", 12 | "version": "0.1", 13 | "changelog": {} 14 | } 15 | -------------------------------------------------------------------------------- /views/dom/domview-legacy/descriptor.json: -------------------------------------------------------------------------------- 1 | { 2 | "friendlyName": "Varv DOM View", 3 | "description": "Legacy view to allow data to be visualized as a browser-based UI (deprecated)", 4 | "dependencies": [ 5 | "#varv-engine" 6 | ], 7 | "assets": [], 8 | "license": "MIT", 9 | "version": "0.1", 10 | "changelog": {} 11 | } 12 | -------------------------------------------------------------------------------- /views/dom/domview-legacy/domview.scss: -------------------------------------------------------------------------------- 1 | dom-view-template { 2 | display: none; 3 | } 4 | 5 | varv-view { 6 | display: contents; 7 | 8 | varv-failure:before { 9 | content: "?"; 10 | width: 1em; 11 | height: 1em; 12 | display: flex; 13 | justify-content: center; 14 | align-items: center; 15 | background: red; 16 | cursor: pointer; 17 | } 18 | 19 | [varv-domview-highlight] { 20 | filter: invert(15%) drop-shadow(0 0 0.25em rgb(100,150,255)); 21 | } 22 | } 23 | .hide-varv-errors { 24 | varv-failure { 25 | display: none; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /views/dom/inspector/descriptor.json: -------------------------------------------------------------------------------- 1 | { 2 | "friendlyName": "Varv engine inspector", 3 | "description": "Inspect DOMView rendering elements using ctrl+rclick", 4 | "dependencies": [ 5 | 6 | ], 7 | "optionalDependencies": [ 8 | "webstrate-components-repos #MaterialDesignOutlinedIcons", 9 | "webstrate-components-repos #MaterialMenu" 10 | ], 11 | "assets": [], 12 | "license": "MIT", 13 | "version": "0.1", 14 | "changelog": {} 15 | } 16 | -------------------------------------------------------------------------------- /views/dom/inspector/inspector.css: -------------------------------------------------------------------------------- 1 | .mdc-menu .varv-inspector-preview { 2 | background: rgba(100,100,100,0.2); 3 | max-width: 30rem; 4 | width: 100%; 5 | overflow: auto; 6 | max-height: 20rem; 7 | padding: 0; 8 | margin: 0; 9 | font-size: 0.8em; 10 | } 11 | -------------------------------------------------------------------------------- /views/dom/react/JSXQueryParseNode.js: -------------------------------------------------------------------------------- 1 | class JSXQueryParseNode extends QueryParseNode { 2 | constructor(reactParamsObject, scopeUpdateCallback){ 3 | super({ 4 | getAttribute: (attribute)=>reactParamsObject.hasOwnProperty(attribute)?reactParamsObject[attribute]:null 5 | }); 6 | this.scopeUpdateCallback = scopeUpdateCallback; 7 | } 8 | 9 | onScopesUpdated(view, newChildScopes){ 10 | this.scopeUpdateCallback(newChildScopes); 11 | } 12 | } 13 | 14 | window.JSXQueryParseNode = JSXQueryParseNode; -------------------------------------------------------------------------------- /views/dom/react/VarvReact.fragment: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | let {useMemo} = React; 3 | 4 | if (!window.VarvScope) window.VarvScope = React.createContext([]); 5 | 6 | export const Varv = (params)=>{ 7 | let [lookupResult,setLookupResult] = React.useState([]); 8 | let scope = React.useContext(VarvScope); 9 | 10 | function onResultsUpdated(scopes){ 11 | setLookupResult(scopes); 12 | } 13 | 14 | React.useEffect(() => { 15 | const queryParser = new JSXQueryParseNode(params, onResultsUpdated); // STUB: maybe reuse? 16 | const view = queryParser.getView(document, scope); 17 | 18 | return () => { 19 | // TODO: Cleanup varv view 20 | view.destroy(); 21 | }; 22 | }, [params.concept, params.property, params.target, params.if, scope]); 23 | 24 | return lookupResult.map(result=>{ 25 | if (result.length==0){ 26 | // Empty scope just forwards children 27 | return params.children; 28 | } else if (result[0].errorMessage){ 29 | console.log(result[0].errorMessage,result[0].ex); 30 | return <varv-failure>{result[0].errorMessage}{result[0].ex.toString()}</varv-failure> 31 | } else { 32 | return <VarvScope.Provider key={result[0].uuid} value={[...scope,...result]}> 33 | {params.children} 34 | </VarvScope.Provider> 35 | } 36 | }); 37 | } 38 | 39 | export function useProperty(lookup){ 40 | let scope = React.useContext(VarvScope); 41 | let [value,setValue] = React.useState(); 42 | let [binding,setBinding] = React.useState(); 43 | 44 | // From React -> Varv 45 | let storeValue = async function sendValueToVarv(value){ 46 | await binding.setValueFor(lookup, value); 47 | } 48 | 49 | // Add a cleanup hook 50 | React.useEffect(() => { 51 | // Fetch the binding from Varv -> React 52 | let binding = DOMView.getBindingFromScope(lookup, scope); 53 | if (!binding) throw new Error("Could not look up "+lookup+" in scope"+JSON.stringify(scope)); 54 | let changeListener = binding.generateRawChangeListener(lookup, null); 55 | changeListener.onChanged = async function updateReactValueFromVarv(newValue){ 56 | if (Array.isArray(newValue)){ 57 | // Make sure not to send the original array as React will remember it and not see changes 58 | setValue(structuredClone(newValue)); 59 | } else { 60 | setValue(newValue); 61 | } 62 | }; 63 | 64 | // Enqueue initial value fetch 65 | let init = binding.getValueFor(lookup); 66 | if (init.then) { 67 | init.then((initialValue)=>{ 68 | setValue(initialValue); 69 | }); 70 | } else { 71 | setValue(init); 72 | } 73 | setBinding(binding); 74 | 75 | return () => { 76 | if (changeListener && changeListener.destroy) changeListener.destroy(); 77 | }; 78 | }, []); 79 | 80 | return [value,storeValue]; 81 | } 82 | 83 | 84 | export function useAction(lookup, actionArguments = {}){ 85 | if (lookup.includes(".")) throw new Error("STUB: Only direct action names supported right now for useAction(), no fancy lookups"); 86 | let scope = React.useContext(VarvScope); 87 | return (additionalActionArguments)=>{ 88 | // Lookup the action in scope 89 | let target = null; 90 | for (let i = scope.length-1; i>=0; i--){ 91 | let scopeEntry = scope[i]; 92 | if (scopeEntry.concept){ 93 | if (!target) target = scopeEntry.uuid; 94 | if (scopeEntry.concept.actions){ // TODO: Also consider other kinds of scopes instead of assuming only Concept/Property 95 | let action = scopeEntry.concept.actions.get(lookup); 96 | if (action) { 97 | action.apply([{target:target}], {...actionArguments, ...additionalActionArguments}); 98 | return; 99 | } 100 | } 101 | } 102 | } 103 | throw new Error("Unknown action "+lookup+" not found in scope"); 104 | }; 105 | } 106 | 107 | -------------------------------------------------------------------------------- /views/dom/react/descriptor.json: -------------------------------------------------------------------------------- 1 | { 2 | "friendlyName": "Varv React View", 3 | "description": "Allow connection to browser-based UI based on React", 4 | "dependencies": [ 5 | "#varv-engine", 6 | "#varv-domdiff-view", 7 | "codestrates-repos #fragment_js_babel" 8 | ], 9 | "assets": [], 10 | "license": "MIT", 11 | "version": "0.1", 12 | "changelog": {} 13 | } 14 | --------------------------------------------------------------------------------