/** Here we separate our aggressiveLayout2 elements. Perhaps we can sub-divide so each element has its own file and only pull in what is needed. But for now we just separate some items. There is a test and example in tests/elements.auto TODO: * Use real functions instead of strings. Comparison is done with == and that is enough to know if the function has changed. If so, only call bind(target) when settings the function as a callback. * diffUpdateElement - css and attributes. The best way to do this is to compare with JSON - we have the previous values so can reset css attributes that no longer exists. */ /** Auto add/diff a listener for an eventType to an element. Defaults to calling functions on the form: (event, element, layout) When using layoutJSON just set: autoListeners: { "click": { function: "clickedItemId", target: this, arguments: [itemID] }} //the callback will then look like this: clickedItemId: function(event, itemID) { }, options can have these arguments: @arg function (String), the name of the function to be called @arg target (Object), who owns the function (defaults to this) @arg wrapperFunction (optional function), to be called first to preprocess the event. Always gets event, element and layout. See textField for example. This way you can prevent the function from being called. @arg arguments (Array, optional), an array of arguments to be applied @arg includeEvent, includeElement, includeLayout (BOOL, optional). If you supply arguments but still want include the default arguments. NOTE: includeEvent is always included! Refactor! example: (linkElement, { function : "tapClickLink", arguments: [issueURL], includeEvent:1, includeElement:1 }, "click") will result in calling: function tapClickLink(event, element, issueURL) You can only have one listener per eventType @return whether or not a listener was added. */ AggressiveLayout.prototype.setListener = function(element, options, eventType) { //create a dedicated place for listeners so we can list and diff them easily. if (!element.autoListeners) element.autoListeners = {} var storedListener = element.autoListeners[eventType]; if (storedListener) { if (options.function) { //remove if not matching var elementEquals = storedListener.arguments var optArgs = options.arguments; if (elementEquals && optArgs) { elementEquals = elementEquals.isEqual(optArgs) } else elementEquals = (elementEquals == optArgs); //both are null or undefined if (elementEquals && storedListener.target == options.target && storedListener.function == options.function) { //they are equal, don't change anything! return false; } } element.removeEventListener(eventType, storedListener.func) element.autoListeners[eventType] = null; } if (!options.function) { //You wanted to remove the function, but it wasn't there return false; } //save and add clickListeners so we can also diff listeners //Wrap the actual function inside this function so we capture closure variables var target = options.target || this if (!target[options.function]) { console.error("target does not have any function called " + options.function); return false; } var method = target[options.function] var layout = this var theFunc = function(event) { //target.lastEvent = options.func && options.func.arguments && options.func.arguments.length > 0 && options.func.arguments[0] //prepend the event if you like it: var args = options.arguments || [] //first make a copy of the array - notice that since we need to slice in backwards order args = args.slice(0) if (options.includeLayout || !options.arguments) args.splice(0, 0, layout) if (options.includeElement || !options.arguments) args.splice(0, 0, element) //event is always included. args.splice(0, 0, event) method.apply(target, args) } if (options.wrapperFunction) { options.func = function (event){ options.wrapperFunction(event, element, layout, theFunc) } } else options.func = theFunc //save and add clickListeners so we can also diff listeners element.autoListeners[eventType] = options; element.addEventListener(eventType, options.func, false); return true; } /** Shorthand to add a dedicated callback for an element, if you have an element with the variable "passwordField" and a callback "passwordFieldDidBlur", this auto-makes a callback when called this.addDedicatedCallback(options.var, "blur", "DidBlur") @param var, the element's name to be used when figuring out the function @param eventType, the event we want to subscribe to. @param target, specify somebody else to get this message than "this" //remember that this is just used for auto-setting callbacks where we have a variable (to be used in the name). //TODO: make it use the setListener function so we can diff it. //TODO: I also like to wrap functions like keyPress, to automatically handle special cases. Figure this out! */ AggressiveLayout.prototype.addDedicatedCallback = function(variable, eventType, functionEnding, target, wrapperFunction) { if (!variable || !functionEnding) return false; if (!this.elements[variable]) return false; if (!target) target = this var didDoFunction = variable + functionEnding if (didDoFunction && target[didDoFunction]) { var element = this.elements[variable]; this.setListener(element, { function : didDoFunction, target : target, wrapperFunction : wrapperFunction }, eventType); //element.addEventListener(eventType, target[didDoFunction].bind(target), false); return true; } return false } /*Shorthand to check for and return the dedicated callback for an element (if exists), without connecting it to any event listeners! like this: you have an element with the variable "passwordField" and a callback called "passwordFieldMyCallback", this makes a callback when called this.getDedicatedCallback(options.var, "MyCallback") */ AggressiveLayout.prototype.getDedicatedCallback = function(variable, functionEnding, target) { if (!variable) return false; if (!target) target = this var didDoFunction = variable + functionEnding if (didDoFunction && target[didDoFunction]) { return target[didDoFunction].bind(target); } return null } /** A uniform behaviour when inserting new children to parents using reference nodes */ AggressiveLayout.prototype.addToParentReference = function(parentNode, element, referenceNode) { //if we have a reference node, we always insert before (since positioning might have changed) if (referenceNode && referenceNode != element) { parentNode.insertBefore(element, referenceNode); } else if (parentNode != element.parentNode) { parentNode.appendChild(element); } } /** Diffs or creates elements, returns the new element if created. NOTE: During regular json-diffing, nodeNames should always match - but first run after generation they might not. Then we do... what? remove element or not? If created by HTML - diffing needs to check document.getElementById(options.var) before creating new elements. */ AggressiveLayout.prototype.diffElement = function(options) { var nodeName = options.type if (!nodeName) { console.error("No nodeName when diffElement!") options.element = this.newDiv(options) return options.element } //if we can't diff an element we must create a new one, and insert it above the previous. //If there is no element we also create a new one if (!options.element && options.var) options.element = this.elements[options.var] //we must use var if exists, or perhaps destroy the old one? var element = options.element if (element && element.nodeName.toUpperCase() != nodeName.toUpperCase()) { //autoTypes supplies their own types, some uses DIV others SELECT, etc element = null } if (!element || this.diffUpdateElement(element, options) === null) { options.element = this.newDiv(options) //this adds to parent as well return options.element } else if (options.parent) { //make sure our parent is the same as element parent! this.addToParentReference(options.parent, element, options.element); } return null } /** we use a dictionary to get named parameters, and can extend inputs without breaking/messing up older code, this way we can also "inherit" the createElement function in other ways like newDiv or newImage below. @param variable, to store it in the elements dictionary for easy access and auto-generated callbacks. @param type what type of element, accepts all that document.createElement accepts. @param html innerHTML @param className all classes this div has @param id the elements id. @param href (for links) @param attributes, set all other attributes, e.g. if you have width, src etc... @param auto_data assosiate this object with any type of data @param listeners, auto add listeners on the form { eventType : options } - see addListener for details on options. Options may be a string, then it is translated into the "function" parameter. @param applyAttributes, to also set the attributes as variables on the element (you need this to make e.g. onchange work) */ AggressiveLayout.prototype.createElement = function(options) { if (!options) options = {} //default to an empty option if (!options.type) options.type = "div"; var element; if (!options.var && options.variable) options.var = options.variable; if (options.var) { //check to see if it exist in our elements dictionary and also if we have server generated HTML element = this.elements[options.var] if (element && this.diffUpdateElement(element, options) === null) { element = document.getElementById(options.var) if (element && this.diffUpdateElement(element, options) === null) element = null; } } if (!element) { //not created - only do those things that are represented in the innerHTML if (options.xmlns) element = document.createElementNS(options.xmlns, options.type); else element = document.createElement(options.type); if (options.html) element.innerHTML = options.html; if (options.className) element.className = options.className; if (options.id) element.id = options.id if (options.position) element.style.position = options.position; if (options.style) { var styles = options.style; for (var styleVariable in styles) { if (styles.hasOwnProperty(styleVariable)) { element.style[styleVariable] = styles[styleVariable]; } } } if (options.href) { //for easy add of links. element.href = options.href; } if (options.attributes) { //set any HTML-attribues, you can also do this simpler with element.attribute = value. Nope! Does not work! var attributes = options.attributes; for (var attributeVariable in attributes) { if (attributes.hasOwnProperty(attributeVariable)) { element.setAttribute(attributeVariable, attributes[attributeVariable]); //this might be dangerous on IE so we have it as opt-in for now. - NOTE: The order matters! if (options.applyAttributes) element[attributeVariable] = attributes[attributeVariable] } } } if (options.auto_data !== undefined) { //sometimes you want an easy connection between the element and its data object - that should not be vissible to the user. This may lead to retain-cycles. element.auto_data = options.auto_data } } //auto-add listeners by supplying a dict with { eventType : options }, these options will be sent to setListener - for more info see that function. if (options.autoListeners) { for (var eventType in options.autoListeners) { if (options.autoListeners.hasOwnProperty(eventType)) { var listener = options.autoListeners[eventType] if (window.isString ? isString(listener) : this.isString(listener)) { listener = { function : listener, includeEvent : 1, includeElement : 1 } } this.setListener(element, listener, eventType) } } } if (options.var) { element.var = options.var; this.elements[options.var] = element; if (element.id != options.var) element.setAttribute("id", options.var); } return element; } /** diffUpdateElement tries to anticipate if there is any changes, and only update those. If there are no changes - it doesn't touch the DOM. This has two benefits: 1, You don't need to remove and re-apply views and layouts when updating, just have variables to your elements and AggressiveLayout knows where in the document it is - and if it needs modifying. 2, Updates are a lot faster. It doesn't really matter how slow the algorithm is that compares your data with the old inside the DOM - if you change the DOM the tiniest bit, it will still be slower. Never touch the DOM unless you have to! NOTE: if you change an element's styles it may not register as a diff in certain cases: e.g when removing a style you must use an empty string, and modifying listeners/listener functions (we don't think about those as layout). TODO: when using variables and then changing the interface (or vice verca), you may get elements in your elements dict that does not have a variable. Think on this, is it a problem or not? - it might be, so why not add the variable to the element? Better safe than sorry? @return BOOL true if changed, null if wrong type. WOW! SOO STUPID! true/false would also be useful! However, we shouldn't need to care here, we should only get to diffUpdate against valid elements. */ AggressiveLayout.prototype.diffUpdateElement = function(element, options) { if (((options.type && options.type.toUpperCase()) || "DIV") != element.nodeName.toUpperCase()) { return null; } var didChange = false; if (!options.var && options.variable) options.var = options.variable; if (options.var) { if (this.elements[options.var] && this.elements[options.var] != element) return null; else if (element.var && element.var != options.var) return null; else if (!this.elements[options.var]) { this.elements[options.var] = element; element.var = options.var; if (element.id != options.var) element.setAttribute("id", options.var); didChange = true; } } else if (element.var) return null; //if we have variable, but the old element didn't we are allowed to steal them - BUT not the other way around. //handle innerHTML - inputs have their text in their value instead of inside their innerHTML (which we should't change anyway) if (element.nodeName == "INPUT" || element.nodeName == "TEXTAREA") { if (!options.attributes) options.attributes = {} if (!options.attributes.value && options.html != null && options.text != null) { //we have to use value in order to get stuff persisted in cache (and also set it as a value!) - TODO: check firefox too! options.value = options.html || options.text options.attributes.value = options.value; } else if (options.attributes.value != null && !options.value) { options.value = options.attributes.value; } } else { //Sadly we must change our thinking, if values are not set we can't assume we want them overwritten. Not setting a value must be considered the same as null. var missmatchText = options.text != element.autoText; if (missmatchText && !options.html && !options.text) { //missmatch where we don't have any text nor html, it means that all the childNodes should be removed. this.removeChildren(element); didChange = true; } else { if (missmatchText && !options.html) { //if one of them has text that missmatchess overwrite if (this.textConversion) { //not yet converted to html - do it here and save in options. options.html = this.textConversion(options.text); if (element.autoJSON) element.autoJSON = null; } else { options.html = options.text } } //else compare HTML from options missmatchText = missmatchText || (!options.text && options.html && options.html != element.innerHTML) /*Old code, looking at innerHTML, but this cannot be valid thinking? Right? we cannot set innerHTML if we have children, or have not changed anything! (and we are not using text) if (!missmatchText && !options.children && !options.hasChildren && !options.text && element.innerHTML && options.html != element.innerHTML) missmatchText = true; //we have no children, but still don't match innerHTML. */ if (missmatchText) { if (!options.text && element.autoText) delete element.autoText else if (element.autoText || options.text) element.autoText = options.text; //reset or set for future compares element.innerHTML = options.html || ""; didChange = true; } } } if (options.parent && options.parent != element.parentNode) { if (options.parent == element) { console.log("Cannot add an element to itself (parent == element). Will crash now and give you a strange error!"); } if (options.element) this.addToParentReference(options.parent, element, options.element); else options.parent.appendChild(element); didChange = true; } if (options.style) { //TODO: compare and set styles, styles missing in options should be removed (set to default value). A HTMLElement has always all styles so this is hard. for (var attributeName in options.style) { if (element.style[attributeName] != options.style[attributeName]) { element.style[attributeName] = options.style[attributeName]; didChange = true; } } } if ((element.className || options.className) && element.className != options.className) { element.className = options.className || ""; didChange = true; } if ((options.href || element.href) && options.href != element.href) { element.href = options.href || ""; didChange = true; } if (options.src || element.src) { //First check if the image is removed if (!options.src && element.src) { //just load an empty image element.src = AUTO_EMPTY_IMAGE didChange = true; } else if (options.src && !element.src) { element.src = options.src element.imageLoadingErrorCount = 0 didChange = true; } else { //it is not enough to just compare their src tag - if they seem to be equal (and img.src are not expanded to an absoluteURL) - you must create an absoluteURL first and compare that! var absoluteURL = null; if (element.src.endsWith(options.src) && options.src != element.src) { absoluteURL = getAbsoluteUrl(options.src) } if (absoluteURL && absoluteURL != element.src) //if I have an absolute URL, it must be equal to the element src... Really? Otherwise they are not equal and we should go here { //first load with an empty image element.src = AUTO_EMPTY_IMAGE element.src = options.src; element.imageLoadingErrorCount = 0 didChange = true; } } } //what is this? a way to associate objects with arbitrary data. if ((options.auto_data || element.auto_data) && options.auto_data != element.auto_data) { //TODO: think about this, is probably always wrong (a new object with old data will missmatch, but reusing an object with new data won't) if (!options.auto_data) delete element.auto_data else element.auto_data = options.auto_data } if (options.autoListeners) { for (var eventType in options.autoListeners) { if (options.autoListeners.hasOwnProperty(eventType)) { var listener = options.autoListeners[eventType] if (window.isString ? isString(listener) : this.isString(listener)) { listener = { function : listener } } this.setListener(element, listener, eventType) } } } if (options.attributes) { //Also diff any HTML-attribues, will only diff what is contained in options. TODO: Also remove what is not contained! var attributes = options.attributes; for (var attributeVariable in attributes) { if (attributes.hasOwnProperty(attributeVariable)) { var currentAttribute = element.getAttribute(attributeVariable) if (!currentAttribute || currentAttribute != attributes[attributeVariable]) { //Note: we remove elements when they are the empty string - will that backfire somewhere? Otherwise we need an additional way to remove them... //it doesn't seem to matter! if (!attributes[attributeVariable]) element.removeAttribute(attributeVariable) else element.setAttribute(attributeVariable, attributes[attributeVariable]); } } } } //If we have an element with value, make sure it also is the same. if ((options.value || element.value) && options.value != element.value) { element.value = options.value || null; } return didChange; } /** create a new div add it to a parent and transform its text to html. If you wish you may change type by setting its option @param text to be converted into html @param parent to automatically add this element to a parent div @param variable, to set this variable when creating and making a diff if the element was already created @param element, to make it only perform a diff - useful in larger apps where you don't want/can keep track of every single element. */ AggressiveLayout.prototype.newDiv = function(options) { if (!options) options = {} //default to an empty option if (options.text === "") //if we use the empty string - we want to remove text { options.html = options.text } //check if it's already created - we are using the variables type if exists. var element = options.element if (element && this.diffUpdateElement(element, options) !== null) { if (element.autoText && element.autoText != options.text) element.autoText = options.text; return element; } //use text convertion if available (after diff - no need to convert if not used), otherwise just plaintext. if (options.text && this.textConversion) { options.html = this.textConversion(options.text); } if (!options.type) options.type = "div"; var element = this.createElement(options); //save the text so we don't need to convert before diff element.autoText = options.text; if (options.parent) this.addToParentReference(options.parent, element, options.element); return element; } /** Create a link like this: { autoType: AUTO_TYPE.anchorLink, key: hashKey, value: hashValue, html : "desc" } to get link: desc when clicked this will now be taken care of by JS - but still displayed and presented like a regular URL. NOTE: We switch to search (?) - so it will be sent to the server if we have no js (for Google, sharing, etc). @param key, they key to set @param value, the value to set @param keysAndValues, set multiple keys and values at the same time. @param hideLinkStyle (bool), hide the appearance of links, so it can look like a button, image, title or anything else you deem suitable. @param scrollToTop (bool), handle scrolling when anchors are changed @param button example, here we close an article with a button, but the click is handled by the anchorLink: { autoType: AUTO_TYPE.anchorLink, hideLinkStyle: 1, key: "post_url", children: [{ html : "Close article", className: ".articleDiv closeButton", autoType: AUTO_TYPE.button, }]} TODO: explain why using it as a wrapper is benefitial sometimes. TODO: detect modifyer keys, and don't block propagation? Do something to fix this! **/ AggressiveLayout.prototype.newAnchorLink = function(options) { if (options.hideLinkStyle && !this.cssStyleSelectorExists(".autoAnchorHideLink")) { this.setCSSFromSelector(".autoAnchorHideLink", { color: "initial", textDecoration: "inherit" }); this.setCSSFromSelector(".autoAnchorHideLink:visited", { color: "initial", textDecoration: "inherit" }); this.setCSSFromSelector(".autoAnchorHideLink:hover", { color: "initial", textDecoration: "inherit" }); } if (options.button) { //To make links look like buttons, you can just style them - or have a button inside as a child. this.setButtonCSS(); if (!options.className) options.className = "plainButton"; else options.className += " plainButton" } options.type = "A" var anchorSignType = "?" var currentAnchor = this.parseQueryString(this.anchor, "#") if (options.key) { if (!options.value) options.href = anchorSignType else options.href = anchorSignType + options.key + "=" + options.value if (currentAnchor) delete currentAnchor[options.key] } else if (options.keysAndValues) { options.href = anchorSignType var keysAndValues = options.keysAndValues for (var key in keysAndValues) { if (options.href.length > 1) options.href += "&" if (keysAndValues.hasOwnProperty(key)) { if (currentAnchor) delete currentAnchor[key] var value = keysAndValues[key]; if (value) options.href += key + "=" + value } } } //if we have multiple selections, the previous ones need to also show when copied/refered to. (but we must be able to overwrite those) if (currentAnchor && options.href) { for (var item in currentAnchor) { if (currentAnchor.hasOwnProperty(item)) { options.href += "&" + item + "=" + currentAnchor[item] } } } if (options.hideLinkStyle) { if (!options.className) options.className = "autoAnchorHideLink"; else options.className += " autoAnchorHideLink" } if (!options.autoListeners) options.autoListeners = {} if (!options.autoListeners.click) { options.autoListeners.click = { function: "anchorLinkHandler", arguments: [options.key, options.value, options.keysAndValues, options.scrollToTop], includeEvent: 1, includeElement: 1, includeLayout: 1 } } else { options.autoListeners.click.wrapperFunction = function(event, element, layout, listenerFunction) { layout.anchorLinkHandler(event, element, layout, options.key, options.value, options.keysAndValues, options.scrollToTop); listenerFunction(event); } } return this.diffElement(options) || options.element } //this works both as a regular function and as a wrapper AggressiveLayout.prototype.anchorLinkHandler = function(event, element, layout, key, value, keysAndValues, scrollToTop) { if (event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) return false; this.blockPropagation(event); //we can't change window.location.search (the query string), since it will cause a reload. BUT do we need that? Instead we can kill the propagation and then just redirect it to hashes. Those can be stored and bookmarked, and when sending to server as the first request they still get seen (verify that please!) if (!key && !keysAndValues) return; //we are allowed to supply no key if we want to handle clicking ourselves var anchor = this.anchor; if (keysAndValues) { for (var key in keysAndValues) { if (keysAndValues.hasOwnProperty(key)) { var value = keysAndValues[key]; anchor = this.modifyValueInQueryString(anchor, key, value, "#") } } } else anchor = this.modifyValueInQueryString(anchor, key, value, "#") if (scrollToTop) window.scroll(window.window.pageXOffset, 0); if (anchor == this.anchor) { //force anchor change, but without modifying history since it should already be there. This is usually due to bugs in your layout code. console.log("anchorLink setting the same anchor twice. Bugs or feature?"); var actions = this.parseQueryString(anchor); this.anchorChangeHandler(event, actions) } else this.setAnchor(anchor, event); } /** Create a dropDownBox (selection). To get the selected value use: element.options[element.selectedIndex].value; @param options need to contain a list of options with name and value and selected (if selected), like options.options = [{ name : "select", value : 0}, { name : "option1", value : 1, selected : 1 }] @param listener String, calls the target (or this) with this function, or the dedicated "_variable_DidChange" method. @param target - the object you want to be called for functions, uses "this" as default */ AggressiveLayout.prototype.newDropDownBox = function(options) { //check if it's already created - then we just change its HTML options.type = "SELECT" var newElement = this.diffElement(options); var element = newElement || options.element //diff children or create new! for (var i = 0; i < options.options.length; i++) { var option = options.options[i] var optionTag = null if (element.childNodes.length > i) { optionTag = element.childNodes[i] } if (!optionTag || optionTag.nodeType != AUTO_NODE_TYPE.ELEMENT_NODE) { optionTag = document.createElement("option"); var result = element.appendChild(optionTag); } if (option.selected) { optionTag.setAttribute('selected', "selected") element.options.selectedIndex = i; } else if (optionTag.getAttribute("selected")) { optionTag.removeAttribute('selected') } if (optionTag.value != option.value) optionTag.value = option.value if (optionTag.innerHTML != option.name) optionTag.innerHTML = option.name } //if created from HTML we must add listeners and such, but the next time we can return here. if (!newElement && element.autoType == options.autoType) return element; if (!options.var && options.variable) options.var = options.variable; if (!element.autoType) element.autoType = options.autoType var target = options.target || this var listener = options.listener; if (listener && target[listener]) { //if we have a name of a function we use that - why would we have a regular function? listener = target[listener].bind(target) } if (listener) { element.addEventListener('change', function (e){ listener(e, element, layout); }, false); } else if (options.var) { this.addDedicatedCallback(options.var, "change", "DidChange", target) } return element; }; /** Create an input type="text" (textField), usually like this: { autoType: AUTO_TYPE.textField, placeholder : "PlaceholderText", allowAutocorrect : 1, allowAutocomplete : 1, allowAutocapitalize : 1, changeOnInput : 1, didReturn : "performLogin" } For didReturn, it will become: this.performLogin.bind(this) @param changeOnInput If you want it to call your elementDidChange method every time the user changes something @param textFieldType, which type of textField you like @param didReturn (String) - the function to call when enter/return is pressed. @param didChange (String) - the function to call when the browser detects changes (supply changeOnInput if you want every keystroke to trigger change) @param didBlur (string) - call this when focus is lost @param target - the object you want to be called for functions, uses "this" as default NOTE: if dedicated functions can be found, those will be used instead of any supplied names. NO! THE OTHER WAY AROUND! TODO: figure out if we should remove preventDidChangeOnBlur AND: UNFOCUS when pressing esc! NOTE: To ensure your HTML input element displays the right AutoFill suggestion, set autocomplete : value. Use the following autocomplete attribute values: Credential Autocomplete Values User Name username Password current-password New Password new-password One-Time Code one-time-code */ AggressiveLayout.prototype.newTextField = function(options) { options.type = "input"; if (!options.attributes) options.attributes = {} options.attributes.type = options.textFieldType || "text"; //there are types of autocomplete, username, new-password etc. Use those. if (options.autocomplete) options.attributes.autocomplete = options.autocomplete; else if (!options.allowAutocomplete) options.attributes.autocomplete = "off"; else options.attributes.autocomplete = "on"; //turn off auto-correction and other keyboard inter-fiddling. We place all of these into the attributes dictionary for easy diffing. if (!options.allowAutocorrect) options.attributes.autocorrect = "off"; else options.attributes.autocorrect = "on"; if (!options.allowAutocapitalize) options.attributes.autocapitalize = "off"; else options.attributes.autocapitalize = "on"; if (options.placeholder) options.attributes.placeholder = options.placeholder; if (options.size) options.attributes.size = options.size; if (options.maxLength) options.attributes.maxLength = options.maxLength; var value = options.value || options.html || options.text || null if (value == null) value = (options.html === "" || options.text === "" || options.value === "") ? "" : null; if (value != null) options.attributes.value = value if (!options.var && options.variable) options.var = options.variable //check if it's already created options.type = "INPUT" var element = this.diffElement(options) || options.element; //if (!element || this.diffUpdateElement(element, options) === null) //{ // var element = this.createElement(options); //} //TODO: rework this below - it can all be diffed! //we can move these to the diffing stage, or we can diff manually like this. var target = options.target || this var method = "keypress"; var wrapperFunction = function(event, element, layout, listenerFunction) { if (event.keyCode == 13) listenerFunction(event, element, layout); } if (options.didReturn !== undefined) this.setListener(element, { function: options.didReturn, target: target, wrapperFunction: wrapperFunction }, method); else this.addDedicatedCallback(options.var, method, "DidReturn", target, wrapperFunction) //if we want changeOnInput we don't send regular change events var method = options.changeOnInput ? "input" : "change"; if (options.didChange !== undefined) this.setListener(element, { function : options.didChange, target : target }, method); else this.addDedicatedCallback(options.var, method, "DidChange", target) //now blur! method = "blur" var blurWasAdded = true if (options.didBlur !== undefined) this.setListener(element, { function : options.didBlur, target : target }, method); else blurWasAdded = this.addDedicatedCallback(options.var, method, "DidBlur", target) //if we have added blur, don't add it for change - also a global helper that can make us never set this. Not sure if it's needed anymore. if (!this.preventDidChangeOnBlur && !blurWasAdded) { if (options.didChange !== undefined) this.setListener(element, { function : options.didChange, target : target }, method); else this.addDedicatedCallback(options.var, method, "DidChange", target) } return element; }; /** Create an input type="textarea" (textArea), usually like this: { autoType: AUTO_TYPE.textArea, placeholder : "PlaceholderText", allowAutocorrect : 1, allowAutocomplete : 1, allowAutocapitalize : 1 } @param changeOnInput If you want it to call your elementDidChange method every time the user changes something @param textFieldType, which type of textField you like */ AggressiveLayout.prototype.newTextArea = function(options) { var layout = this; if (!options.type) options.type = "TEXTAREA"; if (!options.attributes) options.attributes = {} options.attributes.type = options.textFieldType || "text"; //turn off auto-correction and other keyboard inter-fiddling. if (!options.allowAutocomplete) options.attributes.autocomplete = "off"; else options.attributes.autocomplete = "on"; if (!options.allowAutocorrect) options.attributes.autocorrect = "off"; else options.attributes.autocorrect = "on"; if (!options.allowAutocapitalize) options.attributes.autocapitalize = "off"; else options.attributes.autocapitalize = "on"; if (options.placeholder) options.attributes.placeholder = options.placeholder; if (options.cols) options.attributes.cols = options.cols; if (options.rows) options.attributes.rows = options.rows; if (options.maxLength) options.attributes.maxLength = options.maxLength; options.value = options.value || options.html || options.text || ""; if (options.listener === null) { //we may explicitly turned off listeners options.autoListeners = null; } //diff/create var element = this.diffElement(options); if (!element) { return options.element; } //creating a new element, TODO: move this into diff! if (options.listener !== null) //we may explicitly turned off listeners { var target = options.target || this //if we want changeOnInput we don't send regular change events var method = options.changeOnInput ? "input" : "change"; if (options.didChange !== undefined) this.setListener(element, { function : options.didChange, target : target }, method); else this.addDedicatedCallback(options.var, method, "DidChange", target) //now blur! method = "blur" var blurWasAdded = true if (options.didBlur !== undefined) this.setListener(element, { function : options.didBlur, target : target }, method); else blurWasAdded = this.addDedicatedCallback(options.var, method, "DidBlur", target) //if we have added blur, don't add it for change - also a global helper that can make us never set this. Not sure if it's needed anymore. if (!this.preventDidChangeOnBlur && !blurWasAdded) { if (options.didChange !== undefined) this.setListener(element, { function : options.didChange, target : target }, method); else this.addDedicatedCallback(options.var, method, "DidChange", target) } } return element; }; /** Create a checkbox - note that you should wrap it inside a label for best results (and set the style of the label to inlineBlock, otherwise it will not get a size in chrome.) @param name, needed for checkboxes @param checked, if it's checked or not NOTE: all checkboxes needs to be wrapped inside a label, to do this you simply have a label as parent, and a span as sibling. Like this: { type: "label", style : { display: "inlineBlock" }, children: [ { autoType : AUTO_TYPE.checkBox, checked: true, name: theKeyName }, { type : "span", html : " clickable text!"} ] } TODO: look at https://css-tricks.com/the-checkbox-hack/#article-header-id-4 ignore these comments: .autoCheckBoxLabel { position: relative; padding: 10px 0; display: block; cursor: pointer; margin: 0 0 0 34px; border-bottom: 1px solid #b4bcc2; } .autoCheckBoxLabel:last-of-type { border-bottom: 0; } .checkbox-list__check { width: 18px; height: 18px; border: 3px solid #b4bcc2; position: absolute; left: -34px; top: 50%; margin-top: -12px; transition: border .3s ease; border-radius: 5px; } .checkbox-list__check:before { position: absolute; display: block; width: 18px; height: 22px; top: -2px; left: 0px; padding-left: 2px; background-color: transparent; transition: background-color .3s ease; content: '\2713'; font-family: initial; font-size: 19px; color: white; } input[type="checkbox"]:checked ~ .checkbox-list__check { border-color: #5bc0de; } input[type="checkbox"]:checked ~ .checkbox-list__check:before { background-color: #5bc0de; } */ AggressiveLayout.prototype.newCheckbox = function(options) { /* this.applyCSS( { ".autoCheckBox": { borderColor: "green"}, ".autoCheckBox:checked": { borderColor: "blue"}, }); */ if (!options.attributes) options.attributes = {} options.attributes.type = "checkbox"; options.type = "input"; if (!options.className) options.className = "autoCheckBox" else options.className += "autoCheckBox" if (options.listener === null) { //we may explicitly turned off listeners options.autoListeners = null; } if (options.listener && !options.autoListeners) options.autoListeners = { "change" : { function: options.listener, includeEvent : 1, includeElement : 1, target : options.target || null }} if (!options.var && options.variable) options.var = options.variable element = this.newDiv(options) //diff the checkbox if (element.name != options.name) element.name = options.name; if (options.checked && !element.checked) element.checked = 1; else if (!options.checked && element.checked) element.checked = 0; return element; }; /** A SegmentController is just as in iOS - a couple of items where only one can be selected at the same time. Its hard to get them centered - if you don't want it centered you need to excpicitly say so options.disableCenter = true options.segments = ["option one", "option two", "the third option"] To get changes: named function "mySegmentDidClick" NOTE: to programmatically set selections, use: this.segmentControllerSelect(element, index) ADVANCED: @param allowMultipleSelections (bool). we can use this to have multiple selections. @param mutableExclusiveSelections (string) If some cannot be selected at the same time, provide a mutableExclusiveSelections function. A function that takes the controller, and the selectedIndex as parameters, return the bit-pattern that should deselect when this index has been selected: It returns the bit-pattern of the selection that should be deselected (remember that the selection = 0 has bit = 1, and selection = 3 has bit = 8); @param selectedSegmentIndexBits - the selected bits when mutable selections is on. @param selectedSegmentIndex - start with selected index example: { autoType: AUTO_TYPE.segmentController, mutableExclusiveSelections: "segmentsMutable", target: this, segments: [ "the first element", "element number two", "3", "4" ]}, //note how segmentsMutable returns a bit-pattern segmentsMutable: function(controller, index) { console.log("selected " + index) if (index > 1) return 1 + 2 else return 4 + 8 }, */ AggressiveLayout.prototype.segmentClickListener = function(e, element) { var controller = element.parentNode var selectedIndex = controller.value; for (var index = 0; index < controller.childNodes.length; index++) { var segment = controller.childNodes[index]; if (segment == element) { selectedIndex = index; break; } } this.segmentControllerSelect(controller, selectedIndex) if (controller.didClickFunction) controller.didClickFunction(e, controller) } AggressiveLayout.prototype.newSegmentController = function(options) { if (!this.hasSegmentStyles) { this.hasSegmentStyles = 1 //we change to flex, it will break lines - but it will then not look like a segmentController any more. That is better, however - functionality first. You can fix this in JS? flexBasis: "100%" lets you force a newline var selector = this.setCSSFromSelector(".ADSegmentController", { display: "flex", flexWrap: "wrap", justifyContent: "center", cursor: "pointer", marginBottom: "10px", marginTop: "10px", whiteSpace : "nowrap" }) this.preventSelection(selector) var border = "1px solid rgba(0,0,0,0.42)" this.applyCSS( { ".ADSegmentItem": { border: border, borderLeft: "none", }, ".ADSegmentItem:first-child": { borderLeft: border, borderRadius: "5px 0 0 5px" }, ".ADSegmentItem:last-child": { borderRadius: "0 5px 5px 0", }, ".ADSegmentController > div": { display: AUTO_CONST.inlineBlock, padding : "8px", }, }) this.setCSSFromSelector(".ADSegmentController > div:hover", { backgroundColor : "rgb(220, 220, 220)" }) } //just create a div with several other smaller divs inside options.type = "DIV" options.className = "ADSegmentController" if (!options.disableCenter) { if (!options.style) options.style = {} options.style.textAlign = "center"; } if (!options.selectedSegmentIndex && options.selectedSegmentIndex !== 0) options.selectedSegmentIndex = -1 var element = this.diffElement(options) if (element) { //element is created element.selectedSegmentIndexBits = 0; element.selectedSegmentIndex = options.selectedSegmentIndex //we also use value to make it easy to auto-fetch all values in forms element.value = element.selectedSegmentIndex } else { //reusing element element = options.element if (element.selectedSegmentIndex != options.selectedSegmentIndex) { element.selectedSegmentIndex = options.selectedSegmentIndex element.value = element.selectedSegmentIndex console.log("not implemented yet! You are trying to select a new segment with your json, that should work but currently don't") } } element.selectionColor = options.selectionColor || "lightgray" //these things can be overwritten every time if (options.allowMultipleSelections || options.mutableExclusiveSelections) { //if we allow Multiple Selections, we must allow nothing to be selected element.allowMultipleSelections = 1 element.mutableExclusiveSelections = options.mutableExclusiveSelections; //A function that takes the element, and the selectedIndex as parameters, return the bit-pattern that should deselect when this index has been selected } else if (element.allowMultipleSelections) element.allowMultipleSelections = 0 var target = options.target || this element.target = target //diff didClick var didClickFunction = null if (options.didClick && options.didClick != element.didClick) { element.didClick = options.didClick didClickFunction = options.didClick && target[options.didClick] && target[options.didClick].bind(target) } if (!didClickFunction && options.var) { var didClickFunctionName = options.var + "DidClick"; if (element.didClick != didClickFunctionName) { element.didClick = didClickFunctionName didClickFunction = this.elements[options.var] && target[didClickFunctionName] && target[didClickFunctionName].bind(target) } } if (didClickFunction) element.didClickFunction = didClickFunction //let's diff the rest automatically using layoutJSON! var segments = [] for (var index = 0; index < options.segments.length; index++) { var segment = { html: options.segments[index], className: "ADSegmentItem", autoListeners: { "click" : { function: "segmentClickListener", includeEvent: 1, includeElement: 1, target:this }}, } if (!element.allowMultipleSelections && element.selectedSegmentIndex >= 0 && element.selectedSegmentIndex == index) { segment.style = { backgroundColor: element.selectionColor } } segments.push(segment) } this.layoutJSON(element, segments) return element } AggressiveLayout.prototype.segmentControllerSelect = function(controller, selectedIndex) { if (selectedIndex >= controller.childNodes.length) return; var didDeselect = false //we might implement deselection, so we can have nothing selected for us. //if we allow multiple selections, we check mutableExclusiveSelections to see what we need to deselect if (controller.allowMultipleSelections) { var selectedBit = (1 << selectedIndex); if (controller.selectedSegmentIndexBits & selectedBit) { //this was selected - deselect it controller.selectedSegmentIndexBits &= ~selectedBit controller.childNodes[selectedIndex].style.backgroundColor = "" didDeselect = true } else { //new selection controller.selectedSegmentIndexBits |= selectedBit; var func = controller.mutableExclusiveSelections if (func) { var mutableExclusiveSelections = controller.target[func] var deselectBits = mutableExclusiveSelections.call(controller.target, controller, selectedIndex); if (deselectBits) { controller.selectedSegmentIndexBits &= ~deselectBits; for (var i = 0; i < controller.childNodes.length; i++) { var index = (1 << i) & deselectBits; if (index && i < controller.childNodes.length) //we have a match in the bitpattern and should toggle i. { controller.childNodes[i].style.backgroundColor = "" } } } } } //controller.value = controller.selectedSegmentIndexBits; } else if (controller.selectedSegmentIndex >= 0 && controller.selectedSegmentIndex < controller.childNodes.length) { //we might have removed childNodes and can then not unselect them controller.childNodes[controller.selectedSegmentIndex].style.backgroundColor = "" } if (!didDeselect && selectedIndex !== undefined) { if (!controller.allowMultipleSelections) { controller.selectedSegmentIndex = selectedIndex; controller.value = selectedIndex; } controller.childNodes[selectedIndex].style.backgroundColor = controller.selectionColor } } /** We use a dictionary to get named parameters, and can extend inputs without breaking/messing up older code @param tempImageSize supply a size to dispay an empty frame while the image is loading (makes the GUI less "jumpy") @param reflowOnSizeChange default = true, @param src the regular image source */ AggressiveLayout.prototype.newImage = function(options) { if (!options) options = {} //default to an empty option var reflowOnSizeChange = true; reflowOnSizeChange = options.reflowOnSizeChange != undefined && options.reflowOnSizeChange; if (options.size) reflowOnSizeChange = false; if (options.alt) { if (!options.attributes) options.attributes = {} options.attributes.alt = options.alt } if (!options.var && options.variable) options.var = options.variable; options.type = "IMG" var element = this.diffElement(options) || options.element if (element.secondRun) { //we have not created this element, but re-used it and now when the tempImageSize is set, we can return //we must do this so callbacks can get updated with the right img src element.options = options; //Also update the tempSize/reflow settings if we have change the image if (options.tempImageSize && didChange) { //we are only allowed to go here if the image is changed or not already loaded this.setImageTempSize(element, options); } return element; } element.secondRun = true //TODO: REALLY? we must do this so callbacks can get updated with the right img src - I don't know what this means. element.options = options //we can supply an estimation of the image size or we can supply precalculated values if (options.tempImageSize) { //we are only allowed to go here if the image is changed or not already loaded this.setImageTempSize(element, options); } //allow for user defined onload functions if (options.onload) element.auto_onload = options.onload; var layout = this; element.onload = function(event) { if (this.src == layout.imageLoadingError) { //don't adjust layout after error images. return; } if (this.auto_tempSize) { //we remove what was supplied to get the image's real values. If you only supplied one axis, we only remove that. E.g. your width might be set to 40% no matter how big the image is, but the height should be relative. if (this.auto_tempSize.height) this.style.height = "auto"; if (this.auto_tempSize.width) this.style.width = "auto"; //when removing temp-sizes and the original image is in place - if we have new heights/widths we might need to reflow. If you know this isn't needed, turn off reflowOnTempRemove if (this.reflowOnTempRemove && this.clientHeight != this.auto_tempSize.height && this.clientWidth != this.auto_tempSize.width) { if (layout.reflowImageTimeout) window.clearTimeout(layout.reflowImageTimeout); var reflow = function() { //console.log("Triggering reflow timeout!") window.clearTimeout(layout.reflowImageTimeout); layout.updateLayout(); } layout.reflowImageTimeout = setTimeout(reflow, 600); } this.auto_tempSize = false } //element is loaded, check size and perform reflow if needed. if (this.originalSize) { //it was not already loaded before we had time to calculate our new size, we can safely calculate the new one. //we are only interested in widths and heights var reflow = function() { //console.log("Triggering reflow timeout!") window.clearTimeout(this.reflowImageTimeout); this.updateLayout(); }.bind(layout) var newSize = { width : this.clientWidth, height : this.clientHeight } for (var variable in this.originalSize) { if (this.originalSize[variable] != newSize[variable]) { this.originalSize = newSize; if (layout.reflowImageTimeout) window.clearTimeout(layout.reflowImageTimeout); layout.reflowImageTimeout = setTimeout(reflow, 600); break; } } } if (this.auto_onload) this.auto_onload(event, element, layout); delete this.auto_onload; } element.onerror = function() { //replace images on error with a default error-image. if (!layout.imageLoadingError) { //if we can't replace images, don't try. return; } this.imageLoadingErrorCount ++; if (this.imageLoadingErrorCount < 2) //only try once more! { this.src = layout.imageLoadingError; this.src = this.options.src; } else if (this.src != layout.imageLoadingError) { this.src = layout.imageLoadingError } } element.imageLoadingErrorCount = 0; if (reflowOnSizeChange) element.originalSize = { width : element.clientWidth, height : element.clientHeight } //if we have src after onload we can be sure that onload gets called. OR? Yes, I beleive so. if (options.src) element.src = options.src; this.addToParentReference(options.parent, element, options.element); element.auto_isComplete = function () { //we need to check complete in firefox. if (this.complete) return true; if (this.naturalHeight && this.naturalHeight > 0) return true; return false; } return element } AggressiveLayout.prototype.setImageTempSize = function(element, options) { //set auto_tempSize so we know that we have to remove the temporary size when the image has loaded element.auto_tempSize = options.tempImageSize element.reflowOnTempRemove = true; if (options.reflowOnTempRemove != undefined) element.reflowOnTempRemove = options.reflowOnTempRemove; var position = options.tempImageSize var modifier = "px"; if (position.width) { var width = position.width + modifier; if (element.style.width != width) element.style.width = width } if (position.height) { var height = position.height + modifier; if (element.style.height != height) element.style.height = height; } } /** Buttons are just divs with a click callback. Nothing special needed for them or? No, they are buttons. If we like, we should let buttons look like buttons. @warning: elementDidClick in AggressiveLayout will only be called if variableNameDidClick isn't implemented. @param plain, set to true if you want a plain div-button to be styled with CSS @param listener (String), if set will call listener instead of elementDidClick. @param target - the object you want to be called for functions, uses "this" as default @param copyText (optional), to make this button a "copy text" button, all it does is transfer some data to the clipboard. NOTE: We have moved away from elementDidClick since it quickly just becomes large dispatch tables, "jump stations" along the way from one function to the next. */ AggressiveLayout.prototype.newButton = function(options) { if (options.plain) { if (options.className) options.className += " plainButton"; else options.className = "plainButton"; if (!options.type) options.type = "div"; this.setButtonCSS(); } else options.type = "BUTTON"; if (options.listener && !options.autoListeners) options.autoListeners = { "click" : { function: options.listener, includeEvent : 1, includeElement : 1, target : options.target || null }} var element = options.element if (element && element.copyText != options.copyText) element.copyText = options.copyText if (options.copyText) { options.autoListeners = { "click" : { function: "autoCopyText", includeEvent : 1, includeElement : 1, target : this }} } element = this.diffElement(options) || options.element; //if we don't have a dedicated listener, and a variable - set the dedicated one (or diff). if (options.var && (!options.listener || (options.autoListeners && !options.autoListeners.click))) { this.addDedicatedCallback(options.var, "click", "DidClick", (options.target || this)) } return element }; AggressiveLayout.prototype.setButtonCSS = function() { if (!this.plainButtonCSSIsSet) this.plainButtonCSSIsSet = this.cssStyleSelectorExists(".plainButton") if (!this.plainButtonCSSIsSet) { this.plainButtonCSSIsSet = 1; this.setCSSFromSelector(".plainButton:hover", { backgroundColor : "rgb(179, 179, 179)" }) var buttonPadding = 4; var plainButton = this.setCSSFromSelector(".plainButton", { textAlign : "center", display: "inline-block", textDecoration : "none", textTransform : "uppercase", padding : buttonPadding+"px " + 3*buttonPadding+"px " + buttonPadding+"px " + 2*buttonPadding+"px ", border : "1px solid rgba(0,0,0,0.42)", cursor : "pointer" }) this.borderRadius(plainButton, 30) } } AggressiveLayout.prototype.autoCopyText = function(event, element) { var textarea = document.createElement('textarea'); textarea.setAttribute('readonly', true); textarea.setAttribute('contenteditable', true); textarea.contenteditable = true; textarea.style.position = 'absolute'; textarea.style.left = '-9999px'; textarea.value = element.copyText; document.body.appendChild(textarea); var selected = document.getSelection().rangeCount > 0 ? document.getSelection().getRangeAt(0) : false; // create a selectable range var range = document.createRange(); range.selectNodeContents(textarea); // select the range var selection = window.getSelection(); selection.removeAllRanges(); selection.addRange(range); textarea.setSelectionRange(0, 999999); document.execCommand('copy'); document.body.removeChild(textarea); if (selected) { document.getSelection().removeAllRanges(); document.getSelection().addRange(selected); } } /** Build a re-usable table with some data. The options variable must contain "variable" and "data". The data may to be formatted like this { header: [h1, h2...], rows: [ [A1, A2...], [B1, B2...]] } OR if you have dictionaries for your rows (but then you must supply a "keys" array so we can know the order and what values to use) { header: [h1, h2...], rows: [ {key1 : A1, key2 : A2...}, {key1 : B1, key2 : B2...}], keys : [key1, key2, ...] } Every row get the class ADTableRow, every even row get the class ADTableEvenRow Every other row always get the class ADTableOddRow since this is the most common aproach (make different lines have different colors). Additional settings: Note! that for all these functions, rowIndex takes headers into account. If you want to specify classes on cells for specific rows and columns you add a function that returns the exta classNames or an empty string. var classSelector = function(rowIndex, columnIndex) Remember to bind functions to this if you want JS to behave like normally. @param header_long, set a string value to get an extra header on top of your table, spanning all columns. Good for explaining things. To only span certain columns give it a dictionary on the form header_long : { header : [h1, h2, h3...], colSpan : [1, 4, 2...] } @param defaultCellValue, instead of an empty cell (that won't get borders) you can supply a string. Note: there is no special footer or tFoot, do this with a regular data row, and a CSS-class */ AggressiveLayout.prototype.newTable = function(options) { options.type = "TABLE"; var element = this.diffElement(options) || options.element var data = options.data; if (!data) { console.error("No data when creating table!") return element } //Since there are so much stuff going on here, we always check everything. TODO: only add callbacks if rows are new OR element.autoType == undefined var row; var rowIndex = -1; var tableHeader = "TH"; var tableRow = "TR"; var tableData = "TD"; if (!options.classSelector && data.classSelector) options.classSelector = data.classSelector; //sometimes tables has an extra tbody element inside itself which contains the rows. var tbody = null; var originalElement = element; if (element.hasChildNodes() && element.childNodes.length == 1 && element.childNodes[0].nodeName == "TBODY") { tbody = element.childNodes[0]; element = tbody; } if (data.header_long) { element.has_header_long = true rowIndex++; if (data.header_long.header && data.header_long.colSpan) { //we have got a header_long : { header : [h1, h2, h3...], colSpan : [1, 4, 2...] } if (!element.hasChildNodes() || element.childNodes.length <= rowIndex) { row = this.createElement({ type : tableRow }) element.appendChild(row); } else { row = element.childNodes[rowIndex]; } this.newTableRow(rowIndex, row, data.header_long.header, tableHeader, null, options, data.header_long.colSpan); } else //if (isString(data.header_long)) { //the header_long is a simple text var colSpan = data.header && data.header.length if (!colSpan) colSpan = data.rows && data.rows.length if (!colSpan) colSpan = data.keys && data.keys.length if (!element.hasChildNodes() || element.childNodes.length <= rowIndex) { row = this.createElement({ type : tableRow }) element.appendChild(row); var cell = this.newTableCell(rowIndex, 0, row, tableHeader, data.header_long, options); cell.setAttribute("colSpan", colSpan); } else { row = element.childNodes[rowIndex]; var cell = row.childNodes[0]; if (cell.colSpan != colSpan) { cell.setAttribute("colSpan", colSpan); } } } } else if (element.has_header_long) { //remove header_long TODO: check if it also removes the advanced long-header element.has_header_long = false if (element.hasChildNodes() && element.childNodes.length) { row = element.childNodes[rowIndex+1]; row.removeAttribute("colSpan") while (row.hasChildNodes() && row.childNodes.length) { row.removeChild(row.childNodes[0]); } } } if (data.header) { rowIndex++; if (!element.hasChildNodes() || element.childNodes.length <= rowIndex) { row = this.createElement({ type : tableRow }) element.appendChild(row); } else { row = element.childNodes[rowIndex]; } this.newTableRow(rowIndex, row, data.header, tableHeader, null, options); } if (data.rows) { //create a new row for each array in the "rows" var className; var findRowForTargetNode = function(target) { for (var i = element.childNodes.length - 1; i >= 0; i--) { var child = element.childNodes[i] if (child == target) { return { targetIndex: i, target: child } } else if (child == target.parentNode) { return { targetIndex: i, target: child } } else if (target.parentNode && child == target.parentNode.parentNode) { return { targetIndex: i, target: child } } } return null; } for (var index = 0; index < data.rows.length; index++) { var rowCells = data.rows[index]; rowIndex++; if (!element.hasChildNodes() || element.childNodes.length <= rowIndex) { var attributes = null if (options.draggableRow) attributes = { draggable : "true" } row = this.createElement({ type : tableRow, attributes : attributes }) if (rowIndex % 2 == 0) row.className = "ADTableRow ADTableOddRow"; else row.className = "ADTableRow ADTableEvenRow"; element.appendChild(row); } else { row = element.childNodes[rowIndex]; } //we are diffing the listeners too. if (options.rowListener !== undefined) { this.setListener(row, { function : options.rowListener, target : options.target || this, arguments : [index], includeEvent : 1 }, 'click') } if (options.draggableRow) { this.setCSSFromSelector(".dragOverClass", { color : "rgba(0,0,0,0)"}) var layout = this; row.addEventListener("dragstart", function(event) { var target = event.target var tuple = findRowForTargetNode(target) if (!tuple) return; var targetIndex = tuple.targetIndex; layout.currentDragData = { start : targetIndex, height : target.scrollHeight } event.dataTransfer.setData("text/plain", targetIndex); event.dataTransfer.effectAllowed = "move"; if (targetIndex && !event.dataTransfer.getData("text/plain")) event.dataTransfer.setData("text", targetIndex); }) row.addEventListener("dragover", function(event) { event.preventDefault(); }) row.addEventListener("dragenter", function(event) { event.preventDefault(); var target = event.target //now reshuffle all elements and set their temporary locations. var source = element.childNodes[layout.currentDragData.start]; if (!source) { console.log("error no source element!"); return } var modifiedElement = layout.currentDragData.modified; if (modifiedElement) modifiedElement.removeClass("dragOverClass") var tuple = findRowForTargetNode(target) if (!tuple) return; target = tuple.target; var targetIndex = tuple.targetIndex; if (layout.currentDragData.start == targetIndex || source == target || target == modifiedElement) return; layout.addClass(target, "dragOverClass") layout.currentDragData.modified = target; }) row.addEventListener("drop", function(event) { event.preventDefault(); var sourceIndex = layout.currentDragData.start; var source = element.childNodes[sourceIndex]; var modifiedElement = layout.currentDragData.modified; if (modifiedElement) modifiedElement.removeClass("dragOverClass"); var target = event.target var tuple = findRowForTargetNode(target) if (!tuple) return; target = tuple.target; var targetIndex = tuple.targetIndex; if (layout.currentDragData.start == targetIndex) return; element.removeChild(source); if (targetIndex < sourceIndex) element.insertBefore(source, target) else element.insertBefore(source, target.nextSibling) //now change the original data if (options.draggableRowFunction) { var target = options.target || layout; if (data.header) { sourceIndex--; targetIndex--; } target[options.draggableRowFunction](element, sourceIndex, targetIndex); } layout.currentDragData = null; }) } //fill this row with the rowCells this.newTableRow(rowIndex, row, rowCells, tableData, data.keys, options); } } //if we have changed our data to use less rows, we need to remove the surplus: rowIndex++ while (element.hasChildNodes() && element.childNodes.length > rowIndex) { row = element.childNodes[rowIndex]; element.removeChild(row); } originalElement.autoType = options.autoType; return originalElement; } AggressiveLayout.prototype.newTableRow = function(rowIndex, element, row, type, rowKeys, options, colSpan) { if (element.hasChildNodes()) { var rowLength = (rowKeys && rowKeys.length) || row.length if (options.data.header) { var headerLength = options.data.header.length rowLength = headerLength > rowLength ? headerLength : rowLength } for (var i = element.childNodes.length - 1; i >= rowLength; i--) { var cell = element.childNodes[i]; element.removeChild(cell) } } if (rowKeys && !row.length) { //we have a dictionary! for (var index = 0; index < rowKeys.length; index++) { var key = rowKeys[index]; var html = row[key]; this.newTableCell(rowIndex, index, element, type, html, options, colSpan); } } else { //We have an array! for (var index = 0; index < row.length; index++) { var html = row[index]; this.newTableCell(rowIndex, index, element, type, html, options, colSpan); } } } //TODO: make an example of how to create tables - I forget every time! AggressiveLayout.prototype.newTableCell = function(rowIndex, columnIndex, element, type, html, options, colSpan) { if (!html && options.defaultCellValue) html = options.defaultCellValue; var cell; if (!element.hasChildNodes() || element.childNodes.length <= columnIndex) { cell = this.createElement({ type : type, html : html}) var className = options.classSelector && options.classSelector(rowIndex, columnIndex) if (className) { cell.className = className } if (colSpan) cell.setAttribute("colSpan", colSpan[columnIndex]); element.appendChild(cell); } else { cell = element.childNodes[columnIndex]; if (cell.nodeName != type) cell.nodeName = type; //I don't think this can happen, and if it does, not shure how it will be handled. if (cell.innerHTML != html) cell.innerHTML = html; if (options.classSelector) { var className = options.classSelector(rowIndex, columnIndex); if (className && className != cell.className) cell.className = className; else if (cell.className && !className) cell.className = "" } if (colSpan && (!cell.getAttribute("colSpan") || cell.getAttribute("colSpan") != colSpan[columnIndex]) ) cell.setAttribute("colSpan", colSpan[columnIndex]); } if (options && options.cellListener) { //if we have a header we need to subtract one. var adjustedRowIndex = options.data.header ? rowIndex - 1 : rowIndex; this.setListener(cell, { function : options.cellListener, arguments : [adjustedRowIndex, columnIndex], includeEvent : 1, target: options.target }, 'click') } return cell; } /** //Upload divs are just like regular divs, created as such - but will check for drag and drop, and upload files dragged into them. NOTE: You cannot have sub-elements - it will disrupt drag/drop. Images and //to handle callbacks, just inplement a function: element.id + "SuccessCallback". Like if the element's id is dragDropZone, dragDropZoneSuccessCallback() @param allowedFiles, e.g ["image/png", "image/jpeg", "image/gif"] @param maxImageSize, if ImageTools are included it auto-shrinks the image to these sizes (if larger). @param postDictionary, params to supply to postAPI so the server knowns what to do on the receiving end. Note that these comes to the server as POST, like so: $uploadParameters = $_POST['uploadParameters']; (handled automatically by API_2) @param apiOptions, options to supply postAPI when uploading, like success/error callbacks, target, etc. @param preUploadCallback, called with the element and the file(s) right after a file has been assigned. */ AggressiveLayout.prototype.newUploadDiv = function(options) { if (!this.uploadCapabilities) { if ((typeof FileReader == 'undefined') || !('draggable' in document.createElement('span')) || !window.FormData || !("upload" in new XMLHttpRequest())) { this.uploadCapabilities = 2; alert("Error: Could not upload. HTML5 is not supported by this browser"); return; } this.uploadCapabilities = 1; } if (this.uploadCapabilities != 1) return; if (!options) options = {} //default to an empty option if (options.className) options.className += " autoDragDropArea" else options.className = "autoDragDropArea"; if (!options.type) options.type = "DIV"; var element = this.diffElement(options); if (!element) { return options.element; } if (!element.id) { console.error("All uploadDivs must have an id"); return; } if (!this.currentUploads) { this.currentUploads = {} //we set min size on these since they are usually empty. var dragDropAreaCSS = this.setCSSFromSelector(".autoDragDropArea", { backgroundColor: "grey", minHeight: "128px", minWidth: "128px", padding: "10px", cursor : "pointer" }) //think about adding images: //this.setCSSFromSelector(".autoDragDropArea", { backgroundImage: "url(../images/dropZoneImage.png)", backgroundRepeat: "no-repeat", backgroundPosition: "center center" }) this.setShadow(dragDropAreaCSS, "inset 0px 1px 10px rgba(0, 0, 0, 0.7)") this.setCSSFromSelector(".autoDragDropAreaActive", { backgroundColor: "white" }) } //TODO: diff these, no - but don't add these more than once //If you don't supply callbacks, uploadFile will try to AUTO-create those for you on the form element.id + "SuccessCallback". this.currentUploads[element.id] = { allowedFiles : options.allowedFiles, maxImageSize : options.maxImageSize, symetricalImage: options.symetricalImage, postDictionary : options.postDictionary, apiOptions : options.apiOptions, url : options.url, options : options } element.addEventListener("drop", this.dropUpload.bind(this), false); element.addEventListener("click", this.clickUpload.bind(this), false); element.addEventListener("dragover", this.dragOverUpload.bind(this), false); element.addEventListener("dragenter", this.dragEnterUpload.bind(this), false); element.addEventListener("dragleave", this.dragLeaveUpload.bind(this), false); element.addEventListener("dragend", this.dragLeaveUpload.bind(this), false); return element; } AggressiveLayout.prototype.uploadFile = function(e, file, element) { if (!element) element = e.currentTarget; var options = this.currentUploads[element.id]; if (options.request) { element.innerHTML = "You may only upload one file at a time. Please wait until finished, or click here to cancel the upload." return; } if (options.allowedFiles && options.allowedFiles.indexOf("image/jpg") != -1 && options.allowedFiles.indexOf("image/jpeg") == -1) { options.allowedFiles.push("image/jpeg"); } if (options.allowedFiles && options.allowedFiles.indexOf(file.type) == -1) { element.innerHTML = "This (" + file.name + " " + file.type + ") did not look like an allowed file. Please use on of the following: " + options.allowedFiles.join(", ") return; } if (options.preUploadCallback && options.preUploadCallback(e, file, element) === false) return; var formData = new FormData(); element.innerHTML = "Uploading (click to cancel)."; var progress = options.progress if (!progress) { progress = this.createElement({ type: 'progress', style: { width : "100%", padding: "5px" }}); progress.min = 0; progress.max = 100; progress.value = 0; element.appendChild(progress); options.progress = progress } else if (progress.parentNode != element) { element.appendChild(progress); } if (element.className.indexOf(" autoDragDropAreaActive") == -1) { //let the dropzone remain active while we upload element.className += " autoDragDropAreaActive" } //add some options to the request so we can see it will happen if (!options.url) options.url = this.API_URL var apiOptions = options.apiOptions || {} apiOptions.progress = progress; apiOptions.formData = formData; var target = apiOptions.originalTarget || options.target || apiOptions.target || this var successCallback = apiOptions.originalSuccessCallback || apiOptions.successCallback || options.successCallback if (!successCallback) { var didDoFunction = element.id + "SuccessCallback" successCallback = target[didDoFunction] && didDoFunction } apiOptions.target = element; apiOptions.originalTarget = target //store target so we can repeat it the next time element._successCallback = function(result, request) { element.innerHTML = "Upload complete"; if (successCallback) target[successCallback](result, request) } apiOptions.originalSuccessCallback = successCallback //store this so we can repeat it the next time apiOptions.successCallback = "_successCallback" var errorCallback = apiOptions.originalErrorCallback || apiOptions.errorCallback || options.errorCallback if (!errorCallback) { var didDoFunction = element.id + "ErrorCallback" errorCallback = target[didDoFunction] && didDoFunction } element._errorCallback = function(result, request) { element.innerHTML = "Upload failed"; if (errorCallback) target[errorCallback](result, request) } apiOptions.originalErrorCallback = errorCallback apiOptions.errorCallback = "_errorCallback" var imageLoadComplete = function(blob) { //we cannot know/depend on browser support. All API's must always be able to handle the case when nothing works if (blob) formData.append('file', blob); else formData.append('file', file); options.request = this.postAPI(options.postDictionary, apiOptions, options.url) }.bind(this) //we either want and can do image-operations, or we don't want (or can't) and just send it through. if ((options.maxImageSize || options.symetricalImage) && typeof ImageTools !== 'undefined') { var imageTools = new ImageTools(); imageTools.loadImageFile(file, function(image) { var targetSize; var size = { width: image.width, height: image.height } if (options.maxImageSize) { targetSize = imageTools.constrainSize(size, options.maxImageSize) } else targetSize = size; var canvas = document.createElement('canvas'); var ctx = canvas.getContext('2d'); if (options.symetricalImage) { //remember that the api is sourceRect - destinationRect var minSize = Math.min(targetSize.width, targetSize.height) canvas.height = minSize canvas.width = minSize if (size.width > size.height) { //width is larger, cut the left and right edges var extraWidth = Math.floor((size.width - size.height) / 2) ctx.drawImage(image, extraWidth, 0, size.height, size.height, 0, 0, minSize, minSize); } else ctx.drawImage(image, 0, 0, size.width, size.width, 0, 0, minSize, minSize); } else if (options.maxImageSize) { if (targetSize.width == image.width && targetSize.height == image.height) { imageLoadComplete(false) //no need to redraw return; } else { //now we don't have a sourceRect, only destinationRect canvas.height = targetSize.height canvas.width = targetSize.width ctx.drawImage(image, 0, 0, targetSize.width, targetSize.height) } } imageTools.fileBlobFromCanvas(file, canvas, imageLoadComplete) }) } else { imageLoadComplete(false); } } AggressiveLayout.prototype.uploadIsDone = function(element) { var options = this.currentUploads[element.id]; var xmlDoc = options.request if (xmlDoc) { xmlDoc.abort(); //uploadInfo.innerHTML = "Upload was canceled"; if (xmlDoc.progress) { xmlDoc.progress = null; } options.request = null; xmlDoc = null; } } AggressiveLayout.prototype.dropUpload = function(e) { this.blockPropagation(e); if (e.dataTransfer.files.length > 1) { //uploadInfo.innerHTML = "You may only upload one file at a time."; //uploadInfo.style.color = "red"; return; } var file = e.dataTransfer.files[0]; this.uploadFile(e, file); } AggressiveLayout.prototype.clickUpload = function(eventObject) { var options = this.currentUploads[eventObject.currentTarget.id]; if (options && options.progress) return this.cancelUpload(eventObject.currentTarget); var target = eventObject.currentTarget; //create a hidden input that opens a dialog when clicked. This makes it work on iOS if (this.elements.hiddenFileInput) { this.removeFromParent(this.elements.hiddenFileInput); this.elements.hiddenFileInput = null; } var element = this.createElement({ var : "hiddenFileInput", type: "input", attributes : { type : "file" }, style : { visibility : "hidden", position : "absolute", width : "0", height : "0", top: "0" } }) //we only support one file at a time at this moment. //if ((this.options.maxFiles == null) || this.options.maxFiles > 1) // element.setAttribute("multiple", "multiple"); document.body.appendChild(element); //when an image has been selected in the dialog, change will be called. var _this = this; element.addEventListener("change", function(ev) { var file, files, _i, _len; files = element.files; if (files.length) { for (_i = 0, _len = files.length; _i < _len; _i++) { file = files[_i]; _this.uploadFile(eventObject, file, target); } } }); //perform the click on the hidden element element.click(); } AggressiveLayout.prototype.cancelUpload = function(element) { var options = this.currentUploads[element.id]; this.uploadIsDone(element); element.removeClass("autoDragDropAreaActive") if (options.progress) { try { element.removeChild(options.progress) //sometimes it still exists but not here - then exception! FUN! } catch (variable) {} options.progress = null; } element.innerHTML = "" } AggressiveLayout.prototype.dragOverUpload = function(e) { //prevent stupidity, the browser thinks we want to view the file's contents. this.blockPropagation(e); } AggressiveLayout.prototype.dragLeaveUpload = function(e) { e.currentTarget.removeClass("autoDragDropAreaActive") } AggressiveLayout.prototype.dragEnterUpload = function(e) { e.currentTarget.className += " autoDragDropAreaActive" } //here we can handle other problems we will discover with SVG. We might get problems with element.getAttributeNS(), to use instead of the regular element.getAttribute(). AggressiveLayout.prototype.newSVG = function(options) { if (!options.type) options.type = "svg" options.xmlns = "http://www.w3.org/2000/svg" return this.newDiv(options); } /* Create a spinner, and set it where you want stuff to spin. Should center automatically. Instead of having one global spinner, set them like normal elements and remove/hide them when done. Use newTextSpinner if you want a spinner next to text */ AggressiveLayout.prototype.newSpinner = function(options) { if (!this.initSpinner) { //building a nice spinner with svg + css animations this.initSpinner = 1 this.setCSSFromSelector(".autoSpinnerOuter", { height: "20px", width: "20px", textAlign: "center", position: "relative", margin: "auto", }); var frames = {'100%': { transform: "rotate(360deg)" }}; var animation = this.createAnimationCSS("rotationAnimation", frames); if (animation) { //prefix is needed for animations, until 2015 + supported years (5?). var autoSpinnerOuter = this.cssStyleFromSelector(".autoSpinnerOuter"); this.styleWithVendorPrefix(autoSpinnerOuter, "animation", "rotationAnimation .6s linear infinite") } else console.error("Could not create animations!"); } if (!options.className) options.className = "autoSpinnerOuter"; else options.className += " autoSpinnerOuter"; options.type = "DIV" if (options.html) delete options.html var element = this.diffElement(options) || options.element //diffing with json var spinnerEllipseAttributes = { cx: "50%", cy: "50%", rx: "40%", ry: "40%", fill: "transparent", stroke: "#8898aa", "stroke-width": "1.5", "stroke-linecap": "round", "stroke-dasharray": "105% 200%", }; var json = [ { autoType: AUTO_TYPE.svg, type : "svg", className: "spinnerSVG", attributes: { viewBox: "0 0 10 10" }, children: [{ autoType: AUTO_TYPE.svg, type: "ellipse", className: "spinnerEllipse", attributes: spinnerEllipseAttributes }]} ] this.layoutJSON(element, json) return element; } /* Note that textSpinners don't work with markdown, only plain HTML. @param textType, strong, em, or nothing Examples: spinner after div var jsonElement = { children : [{ html : "Fetching...", style: { display: "inline" }}, { autoType: AUTO_TYPE.spinner, style: { display: "inline-block", verticalAlign: "middle", marginRight : "10px", marginLeft : "10px" } }]} or like this: this.loginSpinner = this.layout.newSpinner({ style: { display: "inline-block", verticalAlign: "middle", marginRight : "10px", marginLeft : "10px"} }); this.layout.addToParent(this.loginSpinner, button.parentNode, button); spinner before text var spinnerWithText = [{ autoType : AUTO_TYPE.spinner, style: { cssFloat:"left", marginRight : "10px"}}, { type: "strong", html: "Fetching articles..." }] or with functions this.blogSpinner = this.layout.newDiv({ var: "blogSpinner", style: { display: "block", margin: "auto", maxWidth: layout.maxArticleWidth + "px", paddingLeft : "20px"} }); */ AggressiveLayout.prototype.newTextSpinner = function(options) { if (!this.cssStyleSelectorExists(".autoTextSpinner")) { this.applyCSS( { ".autoTextSpinner": { display: "inline-block", verticalAlign: "middle", marginRight : "10px", marginLeft : "10px" }, ".autoTextSpinnerText": { marginRight : "10px", marginLeft : "10px" }, }); } var element = options.element if (!element) { //transfer all attributes to the container-div, this is not great coding... var container = {} if (options.var) container.var = options.var if (options.className) container.className = options.className element = this.newDiv(container) } var json; if (options.rightSpinner) { json = [ { html: options.html, type: options.textType || "span", className: "autoTextSpinnerText" }, { autoType: AUTO_TYPE.spinner, className: "autoTextSpinner" } ] } else { json = [ { autoType : AUTO_TYPE.spinner, className: "autoTextSpinner" }, { html: options.html, type: options.textType || "span", className: "autoTextSpinnerText" } ] } this.layoutJSON(element, json) this.addToParentReference(options.parent, element, options.element); return element; } /** Makes a tag-cloud that looks like this: _________ __________ |item_1 x | item_2 x | --------- ---------- Where the x's are buttons to click on (to remove them). { autoType: AUTO_TYPE.tagCloud, segments: ["item_1", "item_2"] } TODO: Use SVG for the X instead of a letter. */ AggressiveLayout.prototype.newTagCloud = function(options) { if (!this.initTagCloud) { this.initTagCloud = 1 var selector = this.setCSSFromSelector(".AUTO_TAG_CLOUD_OUTER", { border: "1px solid rgba(0,0,0,0.42)", margin: "2px", whiteSpace: "nowrap", display: AUTO_CONST.inlineBlock, //display: "flex", justifyContent: "center", TODO: change to flex in the future but this is really tricky! }) this.borderRadius(selector, 20) this.preventSelection(selector) var button = this.setCSSFromSelector(".AUTO_TAG_CLOUD_BUTTON", { display: AUTO_CONST.inlineBlock, cursor : "pointer", backgroundColor: "red", textWeight: 900, color: "white", textAlign: "center", padding : "5px", border : "1px solid black", position: "relative", width: "30px" }) this.borderRadius(button, 20) this.setCSSFromSelector(".AUTO_TAG_CLOUD_BUTTON:hover", { backgroundColor : "rgb(220, 220, 220)", color: "black" }) this.setCSSFromSelector(".AUTO_TAG_CLOUD_NAME", { display: AUTO_CONST.inlineBlock, paddingRight: "5px", paddingLeft: "5px" }) } //just create a div with several other smaller divs inside options.type = "DIV" var element = this.diffElement(options) || options.element element.segments = options.segments //auto-diffing with layoutJSON var json = [] for (var index = 0; index < options.segments.length; index++) { var segment = options.segments[index]; var item = { className : "AUTO_TAG_CLOUD_OUTER", children: [ { html : segment, className : "AUTO_TAG_CLOUD_NAME" }, { html: "X", className: "AUTO_TAG_CLOUD_BUTTON", autoListeners: { "click" : { function: "tagCloudListener", includeEvent: 1, includeElement: 1, target:this }}, }, ]} json.push(item) } this.layoutJSON(element, json) //diff didClick var target = options.target || this if (options.didClick && options.didClick != element.didClick) { element.didClick = options.didClick var didClickFunction = options.didClick && target[options.didClick] && target[options.didClick].bind(target) if (!didClickFunction && options.var) { var didClickFunctionName = options.var + "DidClick"; didClickFunction = this.elements[options.var] && target[didClickFunctionName] && target[didClickFunctionName].bind(target) } element.didClickFunction = didClickFunction } if (options.var) { element.didChangeFunction = this.getDedicatedCallback(options.var, "DidChange", target) element.didRemoveFunction = this.getDedicatedCallback(options.var, "DidRemove", target) } return element }; AggressiveLayout.prototype.tagCloudListener = function(event, element) { if (!element.parentNode || !element.parentNode.parentNode) { console.log("error, failing tagCloud structure (button has no parents)") return } var controller = element.parentNode.parentNode var segment = element.parentNode var index = Array.prototype.indexOf.call(controller.children, segment); if (controller.segments && controller.segments.length > index) { var removedSegment = controller.segments.splice(index, 1) if (controller.didRemoveFunction) controller.didRemoveFunction(removedSegment) if (controller.didChangeFunction) controller.didChangeFunction(e) } this.removeFromParent(element.parentNode) };