From 0da1c0baaca02dce320c5ac93490d2c7f4d26514 Mon Sep 17 00:00:00 2001 From: Yvo Brevoort Date: Wed, 12 Mar 2025 17:03:53 +0100 Subject: [PATCH 01/47] update simply-edit and simply.everything --- solid/js/vendor/simplyedit/simply-edit.js | 2259 +++++++------- .../js/vendor/simplyedit/simply.everything.js | 2734 ++++++++++------- 2 files changed, 2797 insertions(+), 2196 deletions(-) diff --git a/solid/js/vendor/simplyedit/simply-edit.js b/solid/js/vendor/simplyedit/simply-edit.js index e140b304..27eb96a9 100644 --- a/solid/js/vendor/simplyedit/simply-edit.js +++ b/solid/js/vendor/simplyedit/simply-edit.js @@ -17,9 +17,6 @@ if (!scriptEl) { scriptEl = document.querySelector("[data-api-key]"); } - if (!scriptEl) { - scriptEl = document.querySelector("[src*='simply-edit.js']"); - } return scriptEl; }; @@ -37,7 +34,7 @@ var scriptURL = document.createElement('a'); scriptURL.href = url; scriptURL.pathname = scriptURL.pathname.replace('simply-edit.js', '').replace(/\/js\/$/, '/'); - if (apiKey !== "" && apiKey !== "muze" && apiKey !== "github") { + if (apiKey !== "") { scriptURL.pathname = scriptURL.pathname + apiKey + "/"; } return scriptURL.href; @@ -103,7 +100,10 @@ var dataFields; if (target.nodeType == document.ELEMENT_NODE && target.getAttribute("data-simply-field")) { dataFields = [target]; - if (target.getAttribute("data-simply-content") === 'fixed') { // special case - if the target field has content fixed, we need to handle its children as well. + if ( + (target.getAttribute("data-simply-content") === 'fixed') || + (target.getAttribute("data-simply-content") === 'attributes') + ) { // special case - if the target field has content fixed or attributes, we need to handle its children as well. var extraFields = target.querySelectorAll("[data-simply-field]"); for (var x=0; x -1); + if (isSub) { + continue; + } + editor.list.init(dataLists[i], listDataItem, useDataBinding); } + if (clone.nodeType == document.ELEMENT_NODE && clone.getAttribute("data-simply-list")) { editor.list.init(clone, listDataItem, useDataBinding); } @@ -825,11 +860,37 @@ list.dataBinding.pauseListeners(list); } + var transformer = list.getAttribute('data-simply-transformer'); + if (transformer) { + if (editor.transformers[transformer] && (typeof editor.transformers[transformer].render === "function")) { + try { + listData = editor.transformers[transformer].render.call(list, listData); + } catch(e) { + console.log("Error thrown in transformer " + transformer); + console.log(e); + } + } else { + console.log("Warning: transformer " + transformer + " is not defined"); + } + } + + if (list.previousValue == JSON.stringify(listData)) { + if (list.dataBinding) { + list.dataBinding.resumeListeners(list); + } + return; // value is the same as the previous time we set it, just keep it; + } + + list.previousValue = JSON.stringify(listData); var previousStyle = list.getAttribute("style"); list.style.height = list.offsetHeight + "px"; // this will prevent the screen from bouncing and messing up the scroll offset. editor.list.clear(list); editor.list.append(list, listData); - list.setAttribute("style", previousStyle); + if (previousStyle) { + list.setAttribute("style", previousStyle); + } else { + list.removeAttribute("style"); + } editor.list.emptyClass(list); if (list.dataBinding) { list.dataBinding.resumeListeners(list); @@ -857,7 +918,7 @@ // Grr... android browser imports the nodes, except the contents of subtemplates. Find them and put them back where they belong. var originalTemplates = template.content.querySelectorAll("template"); - var importedTemplates = clone.querySelectorAll("template"); + var importedTemplates = clone.querySelectorAll("template:not([simply-component])"); for (i=0; i " + newparent); - value._bindings_[subbinding].parentKey = newparent; - if (value[subbinding] && value[subbinding].length) { - for (var i=0; i [data-simply-list-item]"); + //for (i=0; i [data-simply-list-item]"); + for (i=0; i 5) { - console.log("Warning: databinding resolve loop detected!"); + }; + + this.handleEvent = function (event) { + var target = event.currentTarget; + var dataBinding = target.dataBinding; + var elementBinding = target.elementBinding; + + if (typeof dataBinding === 'undefined') { + return; + } + if (dataBinding.paused) { + return; + } + if (target.dataBindingPaused) { + event.stopPropagation(); + return; + } + if (dataBinding.mode === "list") { + if (event.relatedNode && (target != event.relatedNode)) { + return; + } + } + + var i, data, items; + + switch (event.type) { + case "change": + case "databinding:valuechanged": + // Allow the browser to fix what it thinks needs to be fixed (node to be removed, cleaned etc) before setting the new data; + + // these are needed to keep the focus in an element while typing; + elementBinding.pauseListeners(); + dataBinding.set(elementBinding.getter()); + elementBinding.resumeListeners(); + + // these are needed to update after the browser is done doing its thing; window.setTimeout(function() { - binding.resolveCounter = 0; - }, 300); // 300 is a guess; could be any other number. It needs to be long enough so that everyone can settle down before we start resolving again. + elementBinding.pauseListeners(); + dataBinding.set(elementBinding.getter()); + elementBinding.resumeListeners(); + }, 1); // allow the rest of the mutation event to occur; + break; + } + elementBinding.fireEvent("domchanged"); + }; + this.fireEvent = function(event) { + self.dataBinding.fireEvent(self.element, event); + }; + this.fireParent = function(event) { + self.dataBinding.fireEvent(self.element.parentNode, event); + }; + this.isInDocument = function() { + if (document.contains && document.contains(this.element)) { + return true; + } + var parent = element.parentNode; + while (parent) { + if (parent === document) { return true; } - return false; - }; - - var setElements = function() { - if (binding.elementTimer) { - window.clearTimeout(binding.elementTimer); - } - for (var i=0; i -1) { - binding.removeListeners(element); - binding.elements.splice(binding.elements.indexOf(element), 1); - } - }; - - this.cleanupBindings = function() { - if (binding.elements.length < 2) { - return; + shadowValue._bindings_[i] = valueBindings[i]; } - - var inDocument = function(element) { - if (document.contains && document.contains(element)) { - return true; + } + + if (typeof oldValue !== "undefined" && !isEqual(oldValue, shadowValue)) { + binding.config.resolve.call(binding, key, dereference(shadowValue), dereference(oldValue)); + } + //if (typeof shadowValue === "object") { + // shadowValue = dereference(shadowValue); + //} + updateConvertedDataParent(shadowValue); + monitorChildData(shadowValue); + }; + + var updateConvertedDataParent = function(data) { + if ( + binding.config.data._parentBindings_ && + binding.config.data._parentBindings_[binding.key] && + binding.config.data._parentBindings_[binding.key].config.data._simplyListEntryMapping + ) { + var listEntryMapping = binding.config.data._parentBindings_[binding.key].config.data._simplyListEntryMapping; + var convertedParent = binding.config.data._parentBindings_[binding.key].config.data._simplyConvertedParent; + var arrayPaths = binding.config.data._parentBindings_[binding.key].config.data[listEntryMapping]._parentBindings_[binding.key].parentKey.split("/"); + var arrayIndex = arrayPaths.pop(); + arrayIndex = arrayPaths.pop(); + binding.config.data._parentBindings_[binding.key].config.data[binding.key] = data; + var parentData = convertedParent._parentBindings_[arrayIndex].config.data; + var parentKey = arrayPaths.pop(); + parentData[parentKey][arrayIndex][binding.key] = data; + } + }; + + var monitorChildData = function(data) { + // Watch for changes in our child data, because these also need to register as changes in the databound data/elements; + // This allows the use of simple data structures (1 key deep) as databound values and still resolve changes on a specific entry; + var parentData = data; + + if (typeof data === "object") { + var monitor = function(data, key) { + if (!data.hasOwnProperty("_parentBindings_")) { + var bindings = {}; + + Object.defineProperty(data, "_parentBindings_", { + get : function() { + return bindings; + }, + set : function(value) { + bindings[key] = binding; + } + }); + Object.defineProperty(data, "_parentData_", { + get : function() { + return parentData; + } + }); } - var parent = element.parentNode; - while (parent) { - if (parent === document) { - return true; + data._parentBindings_[key] = binding; + + var myvalue = data[key]; + + var renumber = function(key, value, parentBinding) { + var oldparent, newparent; + if (value && value._bindings_) { + for (var subbinding in value._bindings_) { + oldparent = value._bindings_[subbinding].parentKey; + newparent = parentBinding.parentKey + parentBinding.key + "/" + key + "/"; + // console.log(oldparent + " => " + newparent); + value._bindings_[subbinding].parentKey = newparent; + if (value[subbinding] && value[subbinding].length && (typeof value[subbinding] !== "string")) { + for (var i=0; i 5) { + console.log("Warning: databinding resolve loop detected!"); + window.setTimeout(function() { + binding.resolveCounter = 0; + }, 300); // 300 is a guess; could be any other number. It needs to be long enough so that everyone can settle down before we start resolving again. + return true; } + return false; }; - - dataBinding.prototype.addListeners = function(element) { - if (element.dataBinding) { - element.dataBinding.removeListeners(element); + + var setElements = function() { + if (binding.elementTimer) { + window.clearTimeout(binding.elementTimer); } - if (typeof element.mutationObserver === "undefined") { - if (typeof MutationObserver === "function") { - element.mutationObserver = new MutationObserver(this.handleMutation); + for (var i=0; i [data-simply-list-item]"); - for (i=0; i [data-simply-list-item]"); - for (i=0; i -1) { + element.removeListeners(); + binding.elements.splice(binding.elements.indexOf(element), 1); } - self.fireEvent(target, "domchanged"); }; - // Housekeeping, remove references to deleted nodes - document.addEventListener("DOMNodeRemoved", function(evt) { - var target = evt.target; - if (target.nodeType != document.ELEMENT_NODE) { // We don't care about removed text nodes; + this.cleanupBindings = function() { + if (binding.elements.length < 2) { return; } - if (!target.dataBinding) { // nor any element that doesn't have a databinding; - return; + + binding.elements.forEach(function(element) { + if (!element.isInDocument()) { + element.markedForRemoval = true; + } else { + element.markedForRemoval = false; + } + }); + + if (binding.cleanupTimer) { + clearTimeout(binding.cleanupTimer); } - window.setTimeout(function() { // chrome sometimes 'helpfully' removes the element and then inserts it back, probably as a rendering optimalization. We're fine cleaning up in a bit, if still needed. - if (!target.parentNode && target.dataBinding) { - target.dataBinding.unbind(target); - delete target.dataBinding; + + binding.cleanupTimer = window.setTimeout(function() { + binding.elements.filter(function(element) { + if (element.markedForRemoval && !element.isInDocument()) { + element.dataBinding.unbind(element); + return false; + } + element.markedForRemoval = false; + return true; + }); + }, 1000); // If after 1 second the element is still not in the dom, remove the binding; + }; + + initBindings(data, key); + // Call the custom init function, if it is there; + if (typeof binding.config.init === "function") { + binding.config.init.call(binding); + } + + if (binding.mode == "list") { + document.addEventListener("databind:resolved", function() { + if (!binding.skipOldValueUpdate) { + oldValue = dereference(binding.get()); } - }, 1000); - }); - - // polyfill to add :scope selector for IE - (function() { - if (!HTMLElement.prototype.querySelectorAll) { - throw new Error('rootedQuerySelectorAll: This polyfill can only be used with browsers that support querySelectorAll'); - } - - // A temporary element to query against for elements not currently in the DOM - // We'll also use this element to test for :scope support - var container = document.createElement('div'); - - // Check if the browser supports :scope - try { - // Browser supports :scope, do nothing - container.querySelectorAll(':scope *'); - } - catch (e) { - // Match usage of scope - var scopeRE = /\s*:scope/gi; - - // Overrides - function overrideNodeMethod(prototype, methodName) { - // Store the old method for use later - var oldMethod = prototype[methodName]; - - // Override the method - prototype[methodName] = function(query) { - var nodeList, - gaveId = false, - gaveContainer = false; - - if (query.match(scopeRE)) { - if (!this.parentNode) { - // Add to temporary container - container.appendChild(this); - gaveContainer = true; - } - - parentNode = this.parentNode; - - if (!this.id) { - // Give temporary ID - this.id = 'rootedQuerySelector_id_'+(new Date()).getTime(); - gaveId = true; - } - - // Remove :scope - query = query.replace(scopeRE, '#' + this.id + " "); - - // Find elements against parent node - // nodeList = oldMethod.call(parentNode, '#'+this.id+' '+query); - nodeList = parentNode[methodName](query); - // Reset the ID - if (gaveId) { - this.id = ''; - } - - // Remove from temporary container - if (gaveContainer) { - container.removeChild(this); - } - - return nodeList; - } - else { - // No immediate child selector used - return oldMethod.call(this, query); - } - }; - } - - // Browser doesn't support :scope, add polyfill - overrideNodeMethod(HTMLElement.prototype, 'querySelector'); - overrideNodeMethod(HTMLElement.prototype, 'querySelectorAll'); - } - }()); - - editor.init({ - endpoint : document.querySelector("[data-simply-endpoint]") ? document.querySelector("[data-simply-endpoint]").getAttribute("data-simply-endpoint") : null, - toolbars : defaultToolbars, - profile : 'live' + }); + } +}; + +dataBinding.prototype.resumeListeners = function(element) { + element.dataBindingPaused--; + if (element.dataBindingPaused < 0) { + console.log("Warning: resume called of non-paused databinding"); + element.dataBindingPaused = 0; + } + if (element.dataBindingPaused === 0) { + if (element.mutationObserver) { + element.mutationObserver.observe(element, element.mutationObserverConfig); + element.mutationObserver.status = "observing"; + } else { + console.log("Warning: no mutation observer found"); + } + } +}; +dataBinding.prototype.pauseListeners = function(element) { + element.dataBindingPaused++; + if (element.mutationObserver) { + element.mutationObserver.status = "disconnected"; + element.mutationObserver.disconnect(); + } +}; + +// Housekeeping, remove references to deleted nodes +var removalObserver = new MutationObserver(function(mutations) { + mutations.forEach(function(mutation) { + if (mutation.type == "childList") { + mutation.removedNodes.forEach(function(target) { + if (target.nodeType != document.ELEMENT_NODE) { // We don't care about removed text nodes; + return; + } + if (!target.dataBinding) { // nor any element that doesn't have a databinding; + return; + } + window.setTimeout(function() { // chrome sometimes 'helpfully' removes the element and then inserts it back, probably as a rendering optimalization. We're fine cleaning up in a bit, if still needed. + if (!target.parentNode && target.dataBinding && target.elementBinding) { + target.dataBinding.unbind(target.elementBinding); + // if (target.dataBinding.mode == "field") { + // target.dataBinding.set(); + // } + delete target.dataBinding; + } + }, 400); + }); + } }); +}); + +removalObserver.observe(document.body, { + "childList" : true, + "subtree" : true +}); + +// polyfill to add :scope selector for IE +(function() { + if (!HTMLElement.prototype.querySelectorAll) { + throw new Error('rootedQuerySelectorAll: This polyfill can only be used with browsers that support querySelectorAll'); + } + + // A temporary element to query against for elements not currently in the DOM + // We'll also use this element to test for :scope support + var container = document.createElement('div'); + + // Check if the browser supports :scope + try { + // Browser supports :scope, do nothing + container.querySelectorAll(':scope *'); + } + catch (e) { + // Match usage of scope + var scopeRE = /\s*:scope/gi; + + // Overrides + function overrideNodeMethod(prototype, methodName) { + // Store the old method for use later + var oldMethod = prototype[methodName]; + + // Override the method + prototype[methodName] = function(query) { + var nodeList, + gaveId = false, + gaveContainer = false; + + if (query.match(scopeRE)) { + if (!this.parentNode) { + // Add to temporary container + container.appendChild(this); + gaveContainer = true; + } + + parentNode = this.parentNode; + + if (!this.id) { + // Give temporary ID + this.id = 'rootedQuerySelector_id_'+(new Date()).getTime(); + gaveId = true; + } + + // Remove :scope + query = query.replace(scopeRE, '#' + this.id + " "); + + // Find elements against parent node + // nodeList = oldMethod.call(parentNode, '#'+this.id+' '+query); + nodeList = parentNode[methodName](query); + // Reset the ID + if (gaveId) { + this.id = ''; + } + + // Remove from temporary container + if (gaveContainer) { + container.removeChild(this); + } + + return nodeList; + } + else { + // No immediate child selector used + return oldMethod.call(this, query); + } + }; + } + + // Browser doesn't support :scope, add polyfill + overrideNodeMethod(HTMLElement.prototype, 'querySelector'); + overrideNodeMethod(HTMLElement.prototype, 'querySelectorAll'); + } }()); diff --git a/solid/js/vendor/simplyedit/simply.everything.js b/solid/js/vendor/simplyedit/simply.everything.js index ed6561a5..57e66774 100644 --- a/solid/js/vendor/simplyedit/simply.everything.js +++ b/solid/js/vendor/simplyedit/simply.everything.js @@ -1,227 +1,364 @@ -this.simply = (function(simply, global) { - - simply.view = function(app, view) { - - app.view = view || {}; - - var load = function() { - var data = app.view; - var path = editor.data.getDataPath(app.container); - app.view = editor.currentData[path]; - Object.keys(data).forEach(function(key) { - app.view[key] = data[key]; - }); - }; - - if (global.editor && editor.currentData) { - load(); - } else { - document.addEventListener('simply-content-loaded', function() { - load(); - }); - } - - return app.view; - }; +/** + * simply.observe + * This component lets you observe changes in a json compatible data structure + * It doesn't support linking the same object multiple times + * It doesn't register deletion of properties using the delete keyword, assign + * null to the property instead. + * It doesn't register addition of new properties. + * It doesn't register directly assigning new entries in an array on a previously + * non-existant index. + * + * usage: + * + * (function) simply.observe( (object) model, (string) path, (function) callback) + * + * var model = { foo: { bar: 'baz' } }; + * var removeObserver = simply.observe(model, 'foo.bar', function(value, sourcePath) { + * console.log(sourcePath+': '+value); + * }; + * + * The function returns a function that removes the observer when called. + * + * The component can observe in place changes in arrays, either by changing + * an item in a specific index, by calling methods on the array that change + * the array in place or by reassigning the array with a new value. + * + * The sourcePath contains the exact entry that was changed, the value is the + * value for the path passed to simply.observe. + * If an array method was called that changes the array in place, the sourcePath + * also contains that method and its arguments JSON serialized. + * + * sourcePath parts are always seperated with '.', even for array indexes. + * so if foo = [ 'bar' ], the path to 'bar' would be 'foo.0' + */ - return simply; -})(this.simply || {}, this); -this.simply = (function(simply, global) { + /* + FIXME: child properties added after initial observe() call aren't added to the + childListeners. onMissingChildren can't then find them. + TODO: onMissingChildren must loop through all fields to get only the direct child +properties for a given parent, keep seperate index for this? + */ - var routeInfo = []; +(function (global) { + 'use strict'; - function parseRoutes(routes) { - var paths = Object.keys(routes); - var matchParams = /:(\w+|\*)/g; - var matches, params, path; - for (var i=0; ipath.length; + }); + if (!allChildren.length) { + return; + } + var object = getByPath(model, path); + var keysSeen = {}; + allChildren.forEach(function(childPath) { + var key = head(childPath.substr(path.length+1)); + if (typeof object[key] == 'undefined') { + if (!keysSeen[key]) { + callback(object, key, path+'.'+key); + keysSeen[key] = true; + } + } else { + onMissingChildren(model, path+'.'+key, callback); + } + }); + } + + function addChangeListener(model, path, callback) { + if (!changeListeners.has(model)) { + changeListeners.set(model, {}); + } + if (!changeListeners.get(model)[path]) { + changeListeners.get(model)[path] = []; + } + changeListeners.get(model)[path].push(callback); + + if (!parentListeners.has(model)) { + parentListeners.set(model, {}); + } + var parentPath = parent(path); + onParents(model, parentPath, function(parentOb, key, currPath) { + if (!parentListeners.get(model)[currPath]) { + parentListeners.get(model)[currPath] = []; + } + parentListeners.get(model)[currPath].push(path); + }); + + if (!childListeners.has(model)) { + childListeners.set(model, {}); + } + onChildren(model, path, function(childOb, key, currPath) { + if (!childListeners.get(model)[currPath]) { + childListeners.get(model)[currPath] = []; + } + childListeners.get(model)[currPath].push(path); + }); + } + + function removeChangeListener(model, path, callback) { + if (!changeListeners.has(model)) { + return; + } + if (changeListeners.get(model)[path]) { + changeListeners.get(model)[path] = changeListeners.get(model)[path].filter(function(f) { + return f != callback; + }); + } + } + + function pauseObservers() { + observersPaused++; + } + + function resumeObservers() { + observersPaused--; + } + + function attach(model, path, options) { + + var attachArray = function(object, path) { + var desc = Object.getOwnPropertyDescriptor(object, 'push'); + if (!desc || desc.configurable) { + for (var f of ['push','pop','reverse','shift','sort','splice','unshift','copyWithin']) { + (function(f) { + try { + Object.defineProperty(object, f, { + value: function() { + pauseObservers(); + var result = Array.prototype[f].apply(this, arguments); + attach(model, path); + var args = [].slice.call(arguments).map(function(arg) { + return JSON.stringify(arg); + }); + resumeObservers(); + signalChange(model, path, this, path+'.'+f+'('+args.join(',')+')'); + return result; + }, + readable: false, + enumerable: false, + configurable: false + }); + } catch(e) { + console.error('simply.observer: Error: Couldn\'t redefine array method '+f+' on '+path, e); + } + }(f)); + } + for (var i=0, l=object.length; ipath.length; - }); - if (!allChildren.length) { - return; - } - var object = getByPath(model, path); - var keysSeen = {}; - allChildren.forEach(function(childPath) { - var key = head(childPath.substr(path.length+1)); - if (typeof object[key] == 'undefined') { - if (!keysSeen[key]) { - callback(object, key, path+'.'+key); - keysSeen[key] = true; - } - } else { - onMissingChildren(model, path+'.'+key, callback); + }, + init: function(params) { + if (params.root) { + options.root = params.root; } - }); - } - - function addChangeListener(model, path, callback) { - if (!changeListeners.has(model)) { - changeListeners.set(model, {}); - } - if (!changeListeners.get(model)[path]) { - changeListeners.get(model)[path] = []; } - changeListeners.get(model)[path].push(callback); - - if (!parentListeners.has(model)) { - parentListeners.set(model, {}); - } - var parentPath = parent(path); - onParents(model, parentPath, function(parentOb, key, currPath) { - if (!parentListeners.get(model)[currPath]) { - parentListeners.get(model)[currPath] = []; - } - parentListeners.get(model)[currPath].push(path); - }); + }; - if (!childListeners.has(model)) { - childListeners.set(model, {}); + if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { + module.exports = route; + } else { + if (!global.simply) { + global.simply = {}; } - onChildren(model, path, function(childOb, key, currPath) { - if (!childListeners.get(model)[currPath]) { - childListeners.get(model)[currPath] = []; - } - childListeners.get(model)[currPath].push(path); - }); + global.simply.route = route; } +})(this); +(function(global) { + 'use strict'; - function removeChangeListener(model, path, callback) { - if (!changeListeners.has(model)) { - return; - } - if (changeListeners.get(model)[path]) { - changeListeners.get(model)[path] = changeListeners.get(model)[path].filter(function(f) { - return f != callback; - }); - } - } + var listeners = {}; - function pauseObservers() { - observersPaused++; - } - - function resumeObservers() { - observersPaused--; - } - - function attach(model, path, options) { - - var attachArray = function(object, path) { - var desc = Object.getOwnPropertyDescriptor(object, 'push'); - if (!desc || desc.configurable) { - for (var f of ['push','pop','reverse','shift','sort','splice','unshift','copyWithin']) { - (function(f) { - try { - Object.defineProperty(object, f, { - value: function() { - pauseObservers(); - var result = Array.prototype[f].apply(this, arguments); - attach(model, path); - var args = [].slice.call(arguments).map(function(arg) { - return JSON.stringify(arg); - }); - resumeObservers(); - signalChange(model, path, this, path+'.'+f+'('+args.join(',')+')'); - return result; - }, - readable: false, - enumerable: false, - configurable: false - }); - } catch(e) { - console.error('simply.observer: Error: Couldn\'t redefine array method '+f+' on '+path, e); - } - }(f)); - } - for (var i=0, l=object.length; i=0) { + knownCollections[name].splice(index, 1); + } + } + }, + update: function(element, value) { + element.value = value; + element.dispatchEvent(new Event('change', { + bubbles: true, + cancelable: true + })); + } }; - var handleChanges = throttle(function() { - runWhenIdle(function() { - var links = document.querySelectorAll('link[rel="simply-include"],link[rel="simply-include-once"]'); - if (links.length) { - includeLinks(links); + function findCollection(el) { + while (el && !el.dataset.simplyCollection) { + el = el.parentElement; + } + return el; + } + + global.addEventListener('change', function(evt) { + var root = null; + var name = ''; + if (evt.target.dataset.simplyElement) { + root = findCollection(evt.target); + if (root && root.dataset) { + name = root.dataset.simplyCollection; } - }); - }); - - var observe = function() { - observer = new MutationObserver(handleChanges); - observer.observe(document, { - subtree: true, - childList: true, - }); - }; - - observe(); + } + if (name && knownCollections[name]) { + var inputs = root.querySelectorAll('[data-simply-element]'); + var elements = [].reduce.call(inputs, function(elements, input) { + elements[input.dataset.simplyElement] = input; + return elements; + }, {}); + for (var i=knownCollections[name].length-1; i>=0; i--) { + var result = knownCollections[name][i].call(evt.target.form, elements); + if (result === false) { + break; + } + } + } + }, true); - return simply; + if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { + module.exports = collect; + } else { + if (!global.simply) { + global.simply = {}; + } + global.simply.collect = collect; + } -})(this.simply || {}, this); -this.simply = (function(simply, global) { +})(this); +(function(global) { + 'use strict'; var defaultCommands = { 'simply-hide': function(el, value) { @@ -957,6 +951,16 @@ this.simply = (function(simply, global) { { match: 'input,select,textarea', get: function(el) { + if (el.tagName==='SELECT' && el.multiple) { + var values = [], opt; + for (var i=0,l=el.options.length;i { + if (e.isComposing || e.keyCode === 229) { + return; } - if (knownCollections[name].indexOf(callback) == -1) { - knownCollections[name].push(callback); + if (e.defaultPrevented) { + return; } - }, - removeListener: function(name, callback) { - if (knownCollections[name]) { - var index = knownCollections[name].indexOf(callback); - if (index>=0) { - knownCollections[name].splice(index, 1); - } + if (!e.target) { + return; } - }, - update: function(element, value) { - element.value = value; - element.dispatchEvent(new Event('change', { - bubbles: true, - cancelable: true - })); - } - }; - function findCollection(el) { - while (el && !el.dataset.simplyCollection) { - el = el.parentElement; + let selectedKeyboard = 'default'; + if (e.target.closest('[data-simply-keyboard]')) { + selectedKeyboard = e.target.closest('[data-simply-keyboard]').dataset.simplyKeyboard; + } + let key = ''; + if (e.ctrlKey && e.keyCode!=17) { + key+='Control+'; + } + if (e.metaKey && e.keyCode!=224) { + key+='Meta+'; + } + if (e.altKey && e.keyCode!=18) { + key+='Alt+'; + } + if (e.shiftKey && e.keyCode!=16) { + key+='Shift+'; + } + key+=e.key; + + if (keys[selectedKeyboard] && keys[selectedKeyboard][key]) { + let keyboard = keys[selectedKeyboard] + keyboard.app = app; + keyboard[key].call(keyboard,e); + } + }); + + return keys; + } + + + if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { + module.exports = keyboard; + } else { + if (!global.simply) { + global.simply = {}; } - return el; + global.simply.keyboard = keyboard; } - - document.addEventListener('change', function(evt) { - var root = null; - var name = ''; - if (evt.target.dataset.simplyElement) { - root = findCollection(evt.target); - if (root && root.dataset) { - name = root.dataset.simplyCollection; +})(this); +(function(global) { + 'use strict'; + + var defaultActions = { + 'simply-hide': function(el) { + el.classList.remove('simply-visible'); + return Promise.resolve(); + }, + 'simply-show': function(el) { + el.classList.add('simply-visible'); + return Promise.resolve(); + }, + 'simply-select': function(el,group,target,targetGroup) { + if (group) { + this.call('simply-deselect', this.app.container.querySelectorAll('[data-simply-group='+group+']')); } - } - if (name && knownCollections[name]) { - var inputs = root.querySelectorAll('[data-simply-element]'); - var elements = [].reduce.call(inputs, function(elements, input) { - elements[input.dataset.simplyElement] = input; - return elements; - }, {}); - for (var i=knownCollections[name].length-1; i>=0; i--) { - var result = knownCollections[name][i].call(evt.target.form, elements); - if (result === false) { + el.classList.add('simply-selected'); + if (target) { + this.call('simply-select',target,targetGroup); + } + return Promise.resolve(); + }, + 'simply-toggle-select': function(el,group,target,targetGroup) { + if (!el.classList.contains('simply-selected')) { + this.call('simply-select',el,group,target,targetGroup); + } else { + this.call('simply-deselect',el,target); + } + return Promise.resolve(); + }, + 'simply-toggle-class': function(el,className,target) { + if (!target) { + target = el; + } + return Promise.resolve(target.classList.toggle(className)); + }, + 'simply-deselect': function(el,target) { + if ( typeof el.length=='number' && typeof el.item=='function') { + el = Array.prototype.slice.call(el); + } + if ( Array.isArray(el) ) { + for (var i=0,l=el.length; i1 && curr) { - var key = parts.shift(); - if (typeof curr[key] == 'undefined' || curr[key]==null) { - curr[key] = {}; - } - curr = curr[key]; +})(this); +(function(global) { + 'use strict'; + + var resize = function(app, config) { + if (!config) { + config = {}; + } + if (!config.sizes) { + config.sizes = { + 'simply-tiny' : 0, + 'simply-xsmall' : 480, + 'simply-small' : 768, + 'simply-medium' : 992, + 'simply-large' : 1200 + }; } - curr[parts.shift()] = value; - } - function setValue(el, value, binding) { - if (el!=focusedElement) { - var fieldType = getFieldType(binding.fieldTypes, el); - if (fieldType) { - fieldType.set.call(el, (typeof value != 'undefined' ? value : ''), binding); - el.dispatchEvent(new Event('simply.bind.resolved', { - bubbles: true, - cancelable: false - })); + var lastSize = 0; + function resizeSniffer() { + var size = app.container.getBoundingClientRect().width; + if ( lastSize==size ) { + return; + } + lastSize = size; + var sizes = Object.keys(config.sizes); + var match = sizes.pop(); + while (match) { + if ( size=0;i--) { - if (el.matches(setters[i])) { - return binding.fieldTypes[setters[i]].get.call(el); - } + if ( global.attachEvent ) { + app.container.attachEvent('onresize', resizeSniffer); + } else { + global.setInterval(resizeSniffer, 200); } - } - function getFieldType(fieldTypes, el) { - var setters = Object.keys(fieldTypes); - for(var i=setters.length-1;i>=0;i--) { - if (el.matches(setters[i])) { - return fieldTypes[setters[i]]; - } + if ( simply.toolbar ) { + var toolbars = app.container.querySelectorAll('.simply-toolbar'); + [].forEach.call(toolbars, function(toolbar) { + simply.toolbar.init(toolbar); + if (simply.toolbar.scroll) { + simply.toolbar.scroll(toolbar); + } + }); } - return null; - } - function getPath(el, attribute) { - var attributes = attribute.split(','); - for (var attr of attributes) { - if (el.hasAttribute(attr)) { - return el.getAttribute(attr); - } + return resizeSniffer; + }; + + if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { + module.exports = resize; + } else { + if (!global.simply) { + global.simply = {}; } - return null; + global.simply.resize = resize; } +})(this);(function (global) { + 'use strict'; - function throttle( callbackFunction, intervalTime ) { + var throttle = function( callbackFunction, intervalTime ) { var eventId = 0; return function() { var myArguments = arguments; @@ -1236,7 +1373,7 @@ this.simply = (function(simply, global) { }, intervalTime ); } }; - } + }; var runWhenIdle = (function() { if (global.requestIdleCallback) { @@ -1247,165 +1384,649 @@ this.simply = (function(simply, global) { return global.requestAnimationFrame; })(); - function Binding(config, force) { - this.config = config; - if (!this.config) { - this.config = {}; + var rebaseHref = function(relative, base) { + let url = new URL(relative, base) + if (include.cacheBuster) { + url.searchParams.set('cb',include.cacheBuster) + } + return url.href + }; + + var observer, loaded = {}; + var head = global.document.querySelector('head'); + var currentScript = global.document.currentScript; + if (!currentScript) { + var getScriptURL = (function() { + var scripts = document.getElementsByTagName('script'); + var index = scripts.length - 1; + var myScript = scripts[index]; + return function() { return myScript.src; }; + })(); + var currentScriptURL = getScriptURL(); + } else { + var currentScriptURL = currentScript.src; + } + + var waitForPreviousScripts = function() { + // because of the async=false attribute, this script will run after + // the previous scripts have been loaded and run + // simply.include.next.js only fires the simply-next-script event + // that triggers the Promise.resolve method + return new Promise(function(resolve) { + var next = global.document.createElement('script'); + next.src = rebaseHref('simply.include.next.js', currentScriptURL); + next.async = false; + global.document.addEventListener('simply-include-next', function() { + head.removeChild(next); + resolve(); + }, { once: true, passive: true}); + head.appendChild(next); + }); + }; + + var scriptLocations = []; + + var include = { + cacheBuster: null, + scripts: function(scripts, base) { + var arr = []; + for(var i = scripts.length; i--; arr.unshift(scripts[i])); + var importScript = function() { + var script = arr.shift(); + if (!script) { + return; + } + var attrs = [].map.call(script.attributes, function(attr) { + return attr.name; + }); + var clone = global.document.createElement('script'); + attrs.forEach(function(attr) { + clone.setAttribute(attr, script.getAttribute(attr)); + }); + clone.removeAttribute('data-simply-location'); + if (!clone.src) { + // this is an inline script, so copy the content and wait for previous scripts to run + clone.innerHTML = script.innerHTML; + waitForPreviousScripts() + .then(function() { + var node = scriptLocations[script.dataset.simplyLocation]; + node.parentNode.insertBefore(clone, node); + node.parentNode.removeChild(node); + importScript(); + }); + } else { + clone.src = rebaseHref(clone.src, base); + if (!clone.hasAttribute('async') && !clone.hasAttribute('defer')) { + clone.async = false; //important! do not use clone.setAttribute('async', false) - it has no effect + } + var node = scriptLocations[script.dataset.simplyLocation]; + node.parentNode.insertBefore(clone, node); + node.parentNode.removeChild(node); + loaded[clone.src]=true; + importScript(); + } + }; + if (arr.length) { + importScript(); + } + }, + html: function(html, link) { + var fragment = global.document.createRange().createContextualFragment(html); + var stylesheets = fragment.querySelectorAll('link[rel="stylesheet"],style'); + // add all stylesheets to head + [].forEach.call(stylesheets, function(stylesheet) { + if (stylesheet.href) { + stylesheet.href = rebaseHref(stylesheet.href, link.href); + } + head.appendChild(stylesheet); + }); + // remove the scripts from the fragment, as they will not run in the + // order in which they are defined + var scriptsFragment = global.document.createDocumentFragment(); + // FIXME: this loses the original position of the script + // should add a placeholder so we can reinsert the clone + var scripts = fragment.querySelectorAll('script'); + [].forEach.call(scripts, function(script) { + var placeholder = global.document.createComment(script.src || 'inline script'); + script.parentNode.insertBefore(placeholder, script); + script.dataset.simplyLocation = scriptLocations.length; + scriptLocations.push(placeholder); + scriptsFragment.appendChild(script); + }); + // add the remainder before the include link + link.parentNode.insertBefore(fragment, link ? link : null); + global.setTimeout(function() { + if (global.editor && global.editor.data && fragment.querySelector('[data-simply-field],[data-simply-list]')) { + //TODO: remove this dependency and let simply.bind listen for dom node insertions (and simply-edit.js use simply.bind) + global.editor.data.apply(global.editor.currentData, global.document); + } + simply.include.scripts(scriptsFragment.childNodes, link ? link.href : global.location.href ); + }, 10); } - if (!this.config.model) { - this.config.model = {}; + }; + + var included = {}; + var includeLinks = function(links) { + // mark them as in progress, so handleChanges doesn't find them again + var remainingLinks = [].reduce.call(links, function(remainder, link) { + if (link.rel=='simply-include-once' && included[link.href]) { + link.parentNode.removeChild(link); + } else { + included[link.href]=true; + link.rel = 'simply-include-loading'; + remainder.push(link); + } + return remainder; + }, []); + [].forEach.call(remainingLinks, function(link) { + if (!link.href) { + return; + } + // fetch the html + fetch(link.href) + .then(function(response) { + if (response.ok) { + console.log('simply-include: loaded '+link.href); + return response.text(); + } else { + console.log('simply-include: failed to load '+link.href); + } + }) + .then(function(html) { + // if succesfull import the html + simply.include.html(html, link); + // remove the include link + link.parentNode.removeChild(link); + }); + }); + }; + + var handleChanges = throttle(function() { + runWhenIdle(function() { + var links = global.document.querySelectorAll('link[rel="simply-include"],link[rel="simply-include-once"]'); + if (links.length) { + includeLinks(links); + } + }); + }); + + var observe = function() { + observer = new MutationObserver(handleChanges); + observer.observe(global.document, { + subtree: true, + childList: true, + }); + }; + + observe(); + handleChanges(); // check if there are include links in the dom already + + if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { + module.exports = include; + } else { + if (!global.simply) { + global.simply = {}; } - if (!this.config.attribute) { - this.config.attribute = 'data-simply-bind'; + global.simply.include = include; + } + + +})(this); +(function(global) { + 'use strict'; + var view = function(app, view) { + + app.view = view || {}; + + var load = function() { + var data = app.view; + var path = global.editor.data.getDataPath(app.container); + app.view = global.editor.currentData[path]; + Object.keys(data).forEach(function(key) { + app.view[key] = data[key]; + }); + }; + + if (global.editor && global.editor.currentData) { + load(); + } else { + global.document.addEventListener('simply-content-loaded', function() { + load(); + }); } - if (!this.config.selector) { - this.config.selector = '[data-simply-bind]'; + + return app.view; + }; + + if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { + module.exports = view; + } else { + if (!global.simply) { + global.simply = {}; } - if (!this.config.container) { - this.config.container = document; + global.simply.view = view; + } +})(this); +(function(global) { + 'use strict'; + + function etag() { + let d = ''; + while (d.length < 32) d += Math.random().toString(16).substr(2); + const vr = ((parseInt(d.substr(16, 1), 16) & 0x3) | 0x8).toString(16); + return `${d.substr(0, 8)}-${d.substr(8, 4)}-4${d.substr(13, 3)}-${vr}${d.substr(17, 3)}-${d.substr(20, 12)}`; + } + + function ViewModel(name, data, options) { + this.name = name; + this.data = data || []; + this.data.etag = etag(); + this.view = { + options: {}, + data: [] //Array.from(this.data).slice() + }; + this.options = options || {}; + this.plugins = { + start: [], + select: [], + order: [], + render: [], + finish: [] + }; + } + + ViewModel.prototype.update = function(params) { + if (!params) { + params = {}; } - if (typeof this.config.twoway == 'undefined') { - this.config.twoway = true; + if (params.data) { + // this.data is a reference to the data passed, so that any changes in it will get applied + // to the original + this.data = params.data; + this.data.etag = etag() } - this.fieldTypes = { - '*': { - set: function(value) { - this.innerHTML = value; - }, - get: function() { - return this.innerHTML; + // the view is a shallow copy of the array, so that changes in sort order and filtering + // won't get applied to the original, but databindings on its children will still work + this.view.data = Array.from(this.data).slice(); + this.view.data.etag = this.data.etag; + let data = this.view.data; + let plugins = this.plugins.start.concat(this.plugins.select, this.plugins.order, this.plugins.render, this.plugins.finish); + plugins.forEach(plugin => { + data = plugin.call(this, params, data); + if (!data) { + data = this.view.data; + } + this.view.data = data + }); + + if (global.editor) { + global.editor.addDataSource(this.name,{ + load: function(el, callback) { + callback(self.view.data); } + }); + updateDataSource(this.name); + } + }; + + ViewModel.prototype.addPlugin = function(pipe, plugin) { + if (typeof this.plugins[pipe] == 'undefined') { + throw new Error('Unknown pipeline '+pipe); + } + this.plugins[pipe].push(plugin); + }; + + ViewModel.prototype.removePlugin = function(pipe, plugin) { + if (typeof this.plugins[pipe] == 'undefined') { + throw new Error('Unknown pipeline '+pipe); + } + this.plugins[pipe] = this.plugins[pipe].filter(function(p) { + return p != plugin; + }); + }; + + var updateDataSource = function(name) { + global.document.querySelectorAll('[data-simply-data="'+name+'"]').forEach(function(list) { + global.editor.list.applyDataSource(list, name); + }); + }; + + var createSort = function(options) { + var defaultOptions = { + name: 'sort', + getSort: function(params) { + return Array.prototype.sort; } }; - if (this.config.fieldTypes) { - Object.assign(this.fieldTypes, this.config.fieldTypes); - } - this.attach(this.config.container.querySelectorAll(this.config.selector), this.config.model, force); - if (this.config.twoway) { - var self = this; - var observer = new MutationObserver( - throttle(function() { - runWhenIdle(function() { - self.attach(self.config.container.querySelectorAll(self.config.selector), self.config.model); - }); - }) - ); - observer.observe(this.config.container, { - subtree: true, - childList: true - }); + options = Object.assign(defaultOptions, options || {}); + + return function(params) { + this.options[options.name] = options; + if (params[options.name]) { + options = Object.assign(options, params[options.name]); + } + this.view.data.sort(options.getSort.call(this, options)); + }; + }; + + var createPaging = function(options) { + var defaultOptions = { + name: 'paging', + page: 1, + pageSize: 100, + max: 1, + prev: 0, + next: 0 + }; + options = Object.assign(defaultOptions, options || {}); + + return function(params) { + this.options[options.name] = options; + if (this.view.data) { + options.max = Math.max(1, Math.ceil(Array.from(this.view.data).length / options.pageSize)); + } else { + options.max = 1; + } + if (this.view.changed) { + options.page = 1; // reset to page 1 when something in the view data has changed + } + if (params[options.name]) { + options = Object.assign(options, params[options.name]); + } + options.page = Math.max(1, Math.min(options.max, options.page)); // clamp page nr + options.prev = options.page - 1; // calculate previous page, 0 is allowed + if (options.page {}; + cache.$options = Object.assign({}, options); + return new Proxy( cache, getApiHandler(cache.$options) ); + }, - var attachElement = function(jsonPath) { - el.dataset.simplyBound = true; - initElement(el); - setValue(el, getByPath(model, jsonPath), self); - simply.observe(model, jsonPath, function(value) { - if (el != focusedElement) { - setValue(el, value, self); + /** + * Fetches the options.baseURL using the fetch api and returns a promise + * Extra options in addition to those of global.fetch(): + * - user (and password): if set, a basic authentication header will be added + * - paramsFormat: either 'formData', 'json' or 'search' + * By default params, if set, will be added to the baseURL as searchParams + * @param method one of the http verbs, e.g. get, post, etc. + * @param options the options for fetch(), with some additions + * @param params the parameters to send with the request, as javascript/json data + * @return Promise + */ + fetch: function(method, params, options) { + if (!options.url) { + if (!options.baseURL) { + throw new Error('No url or baseURL in options object'); } - }); - }; - - var addMutationObserver = function(jsonPath) { - if (el.dataset.simplyList) { - return; - } - var update = throttle(function() { - runWhenIdle(function() { - var v = getValue(el, self); - var s = getByPath(model, jsonPath); - if (v != s) { - focusedElement = el; - setByPath(model, jsonPath, v); - focusedElement = null; - } - }); - }, 250); - var observer = new MutationObserver(function() { - if (observersPaused) { - return; + while (options.baseURL[options.baseURL.length-1]=='/') { + options.baseURL = options.baseURL.substr(0, options.baseURL.length-1); } - update(); - }); - observer.observe(el, { - characterData: true, - subtree: true, - childList: true, - attributes: true - }); - if (!observers.has(el)) { - observers.set(el, []); + var url = new URL(options.baseURL+options.path); + } else { + var url = options.url; } - observers.get(el).push(observer); - return observer; - }; - - /** - * Runs the init() method of the fieldType, if it is defined. - **/ - var initElement = function(el) { - if (initialized.has(el)) { - return; + var fetchOptions = Object.assign({}, options); + if (!fetchOptions.headers) { + fetchOptions.headers = {}; } - initialized.set(el, true); - var selectors = Object.keys(self.fieldTypes); - for (var i=selectors.length-1; i>=0; i--) { - if (self.fieldTypes[selectors[i]].init && el.matches(selectors[i])) { - self.fieldTypes[selectors[i]].init.call(el, self); - return; + if (params) { + if (method=='GET') { + var paramsFormat = 'search'; + } else { + var paramsFormat = options.paramsFormat; + } + switch(paramsFormat) { + case 'formData': + var formData = new FormData(); + for (const name in params) { + formData.append(name, params[name]); + } + if (!fetchOptions.headers['Content-Type']) { + fetchOptions.headers['Content-Type'] = 'application/x-www-form-urlencoded'; + } + break; + case 'json': + var formData = JSON.stringify(params); + if (!fetchOptions.headers['Content-Type']) { + fetchOptions.headers['Content-Type'] = 'application/json'; + } + break; + case 'search': + var searchParams = url.searchParams; //new URLSearchParams(url.search.slice(1)); + for (const name in params) { + searchParams.set(name, params[name]); + } + url.search = searchParams.toString(); + break; + default: + throw Error('Unknown options.paramsFormat '+options.paramsFormat+'. Select one of formData, json or search.'); + break; } } - }; - - var self = this; - if (el instanceof HTMLElement) { - if (!force && el.dataset.simplyBound) { - return; + if (formData) { + fetchOptions.body = formData } - var jsonPath = getPath(el, this.config.attribute); - if (illegalNesting(el)) { - el.dataset.simplyBound = 'Error: nested binding'; - console.error('Error: found nested data-binding element:',el); - return; + if (options.user) { + fetchOptions.headers['Authorization'] = 'Basic '+btoa(options.user+':'+options.password); } - attachElement(jsonPath); - if (this.config.twoway) { - addMutationObserver(jsonPath); + fetchOptions.method = method.toUpperCase(); + var fetchURL = url.toString() + return fetch(fetchURL, fetchOptions); + }, + /** + * Creates a function to call one or more graphql queries + */ + graphqlQuery: function(url, query, options) { + options = Object.assign({ paramsFormat: 'json', url: url, responseFormat: 'json' }, options); + return function(params, operationName) { + let postParams = { + query: query + }; + if (operationName) { + postParams.operationName = operationName; + } + postParams.variables = params || {}; + return simply.api.fetch('POST', postParams, options ) + .then(function(response) { + return simply.api.getResult(response, options); + }); + } + }, + /** + * Handles the response and returns a Promise with the response data as specified + * @param response Response + * @param options + * - responseFormat: one of 'text', 'formData', 'blob', 'arrayBuffer', 'unbuffered' or 'json'. + * The default is json. + */ + getResult: function(response, options) { + if (response.ok) { + switch(options.responseFormat) { + case 'text': + return response.text(); + break; + case 'formData': + return response.formData(); + break; + case 'blob': + return response.blob(); + break; + case 'arrayBuffer': + return response.arrayBuffer(); + break; + case 'unbuffered': + return response.body; + break; + case 'json': + default: + return response.json(); + break; + } + } else { + throw { + status: response.status, + message: response.statusText, + response: response + } } - } else { - [].forEach.call(el, function(element) { - self.attach(element, model, force); - }); + }, + logError: function(error, options) { + console.error(error.status, error.message); } - }; + } - Binding.prototype.pauseObservers = function() { - observersPaused++; + var defaultOptions = { + path: '', + responseFormat: 'json', + paramsFormat: 'search', + verbs: ['get','post'], + handlers: { + fetch: api.fetch, + result: api.getResult, + error: api.logError + } }; - Binding.prototype.resumeObservers = function() { - observersPaused--; - }; + function cd(path, name) { + name = name.replace(/\//g,''); + if (!path.length || path[path.length-1]!=='/') { + path+='/'; + } + return path+encodeURIComponent(name); + } - simply.bind = function(config, force) { - return new Binding(config, force); - }; + function fetchChain(prop, params) { + var options = this; + return this.handlers.fetch + .call(this, prop, params, options) + .then(function(res) { + return options.handlers.result.call(options, res, options); + }) + .catch(function(error) { + return options.handlers.error.call(options, error, options); + }); + } + + function getApiHandler(options) { + options.handlers = Object.assign({}, defaultOptions.handlers, options.handlers); + options = Object.assign({}, defaultOptions, options); + + return { + get: function(cache, prop) { + if (!cache[prop]) { + if (options.verbs.indexOf(prop)!=-1) { + cache[prop] = function(params) { + return fetchChain.call(options, prop, params); + } + } else { + cache[prop] = api.proxy(Object.assign({}, options, { + path: cd(options.path, prop) + })); + } + } + return cache[prop]; + }, + apply: function(cache, thisArg, params) { + return fetchChain.call(options, 'get', params[0] ? params[0] : null) + } + } + } + + + if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { + module.exports = api; + } else { + if (!global.simply) { + global.simply = {}; + } + global.simply.api = api; + } + +})(this); +(function(global) { + 'use strict'; - return simply; -})(this.simply || {}, this);this.simply = (function(simply, global) { - simply.app = function(options) { + var app = function(options) { if (!options) { options = {}; } @@ -1419,12 +2040,22 @@ this.simply = (function(simply, global) { } if ( options.routes ) { simply.route.load(options.routes); + if (options.routeEvents) { + Object.keys(options.routeEvents).forEach(function(action) { + Object.keys(options.routeEvents[action]).forEach(function(route) { + options.routeEvents[action][route].forEach(function(callback) { + simply.route.addListener(action, route, callback); + }); + }); + }); + } simply.route.handleEvents(); global.setTimeout(function() { simply.route.match(global.location.pathname+global.location.hash); - },1); + }); } - this.container = options.container || document.body; + this.container = options.container || document.body; + this.keyboard = simply.keyboard ? simply.keyboard(this, options.keyboard || {}) : false; this.actions = simply.action ? simply.action(this, options.actions) : false; this.commands = simply.command ? simply.command(this, options.commands) : false; this.resize = simply.resize ? simply.resize(this, options.resize) : false; @@ -1442,187 +2073,16 @@ this.simply = (function(simply, global) { return this.container.querySelector('[data-simply-id='+id+']') || document.getElementById(id); }; - var app = new simplyApp(options); - - return app; - }; - - return simply; -})(this.simply || {}, this); -this.simply = (function(simply, global) { - - var listeners = {}; - - simply.activate = { - addListener: function(name, callback) { - if (!listeners[name]) { - listeners[name] = []; - } - listeners[name].push(callback); - initialCall(name); - }, - removeListener: function(name, callback) { - if (!listeners[name]) { - return false; - } - listeners[name] = listeners[name].filter(function(listener) { - return listener!=callback; - }); - } - }; - - var initialCall = function(name) { - var nodes = document.querySelectorAll('[data-simply-activate="'+name+'"]'); - if (nodes) { - [].forEach.call(nodes, function(node) { - callListeners(node); - }); - } - }; - - var callListeners = function(node) { - if (node && node.dataset.simplyActivate - && listeners[node.dataset.simplyActivate] - ) { - listeners[node.dataset.simplyActivate].forEach(function(callback) { - callback.call(node); - }); - } - }; - - var handleChanges = function(changes) { - var activateNodes = []; - for (var change of changes) { - if (change.type=='childList') { - [].forEach.call(change.addedNodes, function(node) { - if (node.querySelectorAll) { - var toActivate = [].slice.call(node.querySelectorAll('[data-simply-activate]')); - if (node.matches('[data-simply-activate]')) { - toActivate.push(node); - } - activateNodes = activateNodes.concat(toActivate); - } - }); - } - } - if (activateNodes.length) { - activateNodes.forEach(function(node) { - callListeners(node); - }); - } - }; - - var observer = new MutationObserver(handleChanges); - observer.observe(document, { - subtree: true, - childList: true - }); - - return simply; -})(this.simply || {}, this); -this.simply = (function(simply, global) { - var defaultActions = { - 'simply-hide': function(el) { - el.classList.remove('simply-visible'); - return Promise.resolve(); - }, - 'simply-show': function(el) { - el.classList.add('simply-visible'); - return Promise.resolve(); - }, - 'simply-select': function(el,group,target,targetGroup) { - if (group) { - this.call('simply-deselect', this.app.container.querySelectorAll('[data-simply-group='+group+']')); - } - el.classList.add('simply-selected'); - if (target) { - this.call('simply-select',target,targetGroup); - } - return Promise.resolve(); - }, - 'simply-toggle-select': function(el,group,target,targetGroup) { - if (!el.classList.contains('simply-selected')) { - this.call('simply-select',el,group,target,targetGroup); - } else { - this.call('simply-deselect',el,target); - } - return Promise.resolve(); - }, - 'simply-toggle-class': function(el,className,target) { - if (!target) { - target = el; - } - return Promise.resolve(target.classList.toggle(className)); - }, - 'simply-deselect': function(el,target) { - if ( typeof el.length=='number' && typeof el.item=='function') { - el = Array.prototype.slice.call(el); - } - if ( Array.isArray(el) ) { - for (var i=0,l=el.length; i Date: Wed, 12 Mar 2025 17:04:46 +0100 Subject: [PATCH 02/47] update versions --- solid/appinfo/info.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/solid/appinfo/info.xml b/solid/appinfo/info.xml index 16cc1db6..c19f2732 100644 --- a/solid/appinfo/info.xml +++ b/solid/appinfo/info.xml @@ -11,14 +11,14 @@ It supports the webid-oidc-dpop-pkce login flow to connect to a Solid App with y When you do this, the Solid App can store data in your Nextcloud account through the Solid protocol. ]]> - 0.9.1 + 0.9.2 agpl Auke van Slooten Solid integration https://github.com/pdsinterop/solid-nextcloud/issues - + OCA\Solid\Settings\SolidAdmin From 80e3de691407798c0a37ca789deab81f41ec5345 Mon Sep 17 00:00:00 2001 From: Yvo Brevoort Date: Wed, 12 Mar 2025 17:05:07 +0100 Subject: [PATCH 03/47] update versions --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4aa46902..5377bf4a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,9 +34,9 @@ jobs: # Version 24 comes with PHP 8.0, which is no longer supported; # Latest is not tested here, as that could cause failures unrelated to project changes nextcloud_version: - - 28 - 29 - 30 + - 31 steps: - name: Create docker tag from git reference @@ -82,16 +82,16 @@ jobs: fail-fast: false matrix: nextcloud_version: - - 28 - 29 - 30 + - 31 test: - 'solidtestsuite/solid-crud-tests:v7.0.5' - 'solidtestsuite/web-access-control-tests:v7.1.0' - 'solidtestsuite/webid-provider-tests:v2.1.1' # Prevent EOL or non-stable versions of Nextcloud to fail the test-suite - continue-on-error: ${{ contains(fromJson('[28,29,30]'), matrix.nextcloud_version) == false }} + continue-on-error: ${{ contains(fromJson('[29,30,31]'), matrix.nextcloud_version) == false }} steps: - name: Create docker tag from git reference From 751f23b7ccb9104d6f7f8fa7d6214f28c64c23f5 Mon Sep 17 00:00:00 2001 From: Yvo Brevoort Date: Wed, 12 Mar 2025 17:16:38 +0100 Subject: [PATCH 04/47] add file requirement --- solid/templates/applauncher/index.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/solid/templates/applauncher/index.php b/solid/templates/applauncher/index.php index d6fc1d65..bfd38195 100644 --- a/solid/templates/applauncher/index.php +++ b/solid/templates/applauncher/index.php @@ -44,6 +44,14 @@ +