From d28b81a116e410f646e3f52480040dbf44d4b6d1 Mon Sep 17 00:00:00 2001 From: "fiatjaf@spooner" Date: Mon, 28 Mar 2016 23:40:38 -0400 Subject: [PATCH] virtual-dom for saner create-form modal management. --- formspree/static/css/main.css | 3 + formspree/static/js/bundle.js | 1999 +++++++++++++++++++++++++- formspree/static/js/main.js | 70 +- formspree/static/js/sitewide.js | 147 ++ formspree/static/scss/dashboard.scss | 2 + package.json | 4 +- 6 files changed, 2081 insertions(+), 144 deletions(-) create mode 100644 formspree/static/js/sitewide.js diff --git a/formspree/static/css/main.css b/formspree/static/css/main.css index a73cc8cc..5d73e76c 100644 --- a/formspree/static/css/main.css +++ b/formspree/static/css/main.css @@ -791,6 +791,9 @@ a.alert-box.success:hover { .dashboard .modal:not(.js):target:before, .dashboard .modal.target:before { display: block; } +.red { + color: #000; } + .slicknav_btn { position: relative; display: block; diff --git a/formspree/static/js/bundle.js b/formspree/static/js/bundle.js index 4466e488..931ed0cb 100644 --- a/formspree/static/js/bundle.js +++ b/formspree/static/js/bundle.js @@ -1,9 +1,6 @@ (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o' + url.resolve(urlInput.val(), '/' + sitewideFile) + ' exists'); - } else { - // wrong input - if (!urlp.host) { - // invalid url - info.text('Please input a valid URL.'); - } else { - // invalid email - info.text('Please input a valid email address.'); - } - } - - createButton.find('button').prop('disabled', true); - info.css('visibility', 'visible'); - } else { - // toggle sitewide off - info.css('visibility', 'hidden'); - verifyButton.css('visibility', 'hidden'); - createButton.css('visibility', 'visible'); - } - } - - verifyButton.find('button').on('click', function () { - $.ajax({ - url: '/forms/sitewide-check?' + createform.find('form').serialize(), - success: function success() { - toastr.success('The file exists! you can create your site-wide form now.'); - createButton.find('button').prop('disabled', false); - verifyButton.css('visibility', 'hidden'); - info.css('visibility', 'hidden'); - }, - error: function error() { - toastr.warning("The verification file wasn't found."); - verifyButton.find('button').prop('disabled', true); - setTimeout(function () { - verifyButton.find('button').prop('disabled', false); - }, 5000); - } - }); - return false; - }); -} -sitewide(); - /* turning flask flash messages into js popup notifications */ window.popupMessages.forEach(function (m, i) { var category = m[0] || 'info'; @@ -163,7 +94,324 @@ $('a.resend').on('click', function () { $('form.resend').show(); }); -},{"is-valid-email":2,"url":7}],2:[function(require,module,exports){ +/* scripts at other files */ +require('./sitewide')(); + +},{"./sitewide":2}],2:[function(require,module,exports){ +'use strict'; + +var url = require('url'); +var isValidUrl = require('valid-url').isWebUri; +var isValidEmail = require('is-valid-email'); + +var h = require('virtual-dom/h'); +var diff = require('virtual-dom/diff'); +var patch = require('virtual-dom/patch'); +var createElement = require('virtual-dom/create-element'); + +var $ = window.$; +var toastr = window.toastr; + +/* create-form validation for site-wide forms */ +module.exports = function sitewide() { + var parentNode = $('#create-form .container'); + + var formActionURL = parentNode.find('form').attr('action'); + var currentUserEmail = parentNode.find('[name="email"]').val(); + + // since we have javascript, let's trash this HTML and recreate with virtual-dom + parentNode.html(''); + + var data = { + invalid: null, + sitewide: false, + verified: false, + email: currentUserEmail + }; + var tree = render(data); + var rootNode = createElement(tree); + parentNode[0].appendChild(rootNode); + + parentNode.on('change', 'input[name="sitewide"]', run); + parentNode.on('input', 'input[name="url"], input[name="email"]', run); + parentNode.on('click', '.verify button', check); + + function run() { + var checkbox = parentNode.find('input[name="sitewide"]'); + + var email = parentNode.find('input[name="email"]').val().trim(); + var urlv = parentNode.find('input[name="url"]').val().trim(); + var sitewide = checkbox.is(':checked'); + + // wrong input + if (!isValidEmail(email)) { + // invalid email + data.invalid = 'email'; + } else if (sitewide && !isValidUrl(urlv)) { + // invalid url with sitewide + data.invalid = 'url'; + } else if (!sitewide && urlv && !isValidUrl(urlv)) { + // invalid url without sitewide + data.invalid = 'url'; + } else { + data.invalid = null; + } + + data.sitewide = sitewide; + data.urlv = urlv; + data.email = email; + + apply(render(data)); + } + + function check() { + $.ajax({ + url: '/forms/sitewide-check?' + parentNode.find('form').serialize(), + success: function success() { + toastr.success('The file exists! you can create your site-wide form now.'); + data.verified = true; + apply(render(data)); + }, + error: function error() { + toastr.warning("The verification file wasn't found."); + data.verified = false; + data.disableVerification = true; + apply(render(data)); + + setTimeout(function () { + data.disableVerification = false; + apply(render(data)); + }, 5000); + } + }); + + return false; + } + + function apply(vtree) { + var patches = diff(tree, vtree); + rootNode = patch(rootNode, patches); + tree = vtree; + } + + function render(_ref) { + var invalid = _ref.invalid; + var sitewide = _ref.sitewide; + var verified = _ref.verified; + var urlv = _ref.urlv; + var email = _ref.email; + var disableVerification = _ref.disableVerification; + + return h('form', { method: 'post', action: formActionURL }, [h('.col-1-1', [h('h4', 'Send email to:'), h('input', { type: 'email', name: 'email', placeholder: 'You can point this form to any email address', value: email })]), h('.col-1-1', [h('h4', 'From URL:'), h('input', { type: 'url', name: 'url', placeholder: 'Leave blank to send confirmation email when first submitted' })]), h('.container', [h('.col-1-4', [h('label.hint--bottom', { dataset: { hint: 'A site-wide form is a form that you can place on all pages of your website -- and you just have to confirm once!' } }, [h('input', { type: 'checkbox', name: 'sitewide', value: 'true' }), ' site-wide'])]), h('.col-3-4', [invalid ? h('div.red', invalid === 'email' ? 'Please input a valid email address.' : ['Please input a valid URL. For example: ', h('span.code', url.resolve('http://www.mywebsite.com', sitewide ? '' : '/contact.html'))]) : sitewide && verified || !sitewide ? h('div', { innerHTML: '​' }) : h('span', ['Please ensure ', h('span.code', url.resolve(urlv, '/formspree-verify.txt')), ' exists and contains a line with ', h('span.code', email)])]), h('.col-1-3', [h('.verify', [h('button', sitewide && !invalid && !disableVerification ? {} : sitewide ? { disabled: true } : { style: { visibility: 'hidden' }, disabled: true }, 'Verify')])]), h('.col-1-3', { innerHTML: '​' }), h('.col-1-3', [h('.create', [sitewide && verified || !sitewide && !invalid ? h('button', { type: 'submit' }, 'Create form') : h('button', { disabled: true }, 'Create form')])])])]); + } +}; + +},{"is-valid-email":10,"url":15,"valid-url":17,"virtual-dom/create-element":18,"virtual-dom/diff":19,"virtual-dom/h":20,"virtual-dom/patch":21}],3:[function(require,module,exports){ + +},{}],4:[function(require,module,exports){ +/*! + * Cross-Browser Split 1.1.1 + * Copyright 2007-2012 Steven Levithan + * Available under the MIT License + * ECMAScript compliant, uniform cross-browser split method + */ + +/** + * Splits a string into an array of strings using a regex or string separator. Matches of the + * separator are not included in the result array. However, if `separator` is a regex that contains + * capturing groups, backreferences are spliced into the result each time `separator` is matched. + * Fixes browser bugs compared to the native `String.prototype.split` and can be used reliably + * cross-browser. + * @param {String} str String to split. + * @param {RegExp|String} separator Regex or string to use for separating the string. + * @param {Number} [limit] Maximum number of items to include in the result array. + * @returns {Array} Array of substrings. + * @example + * + * // Basic use + * split('a b c d', ' '); + * // -> ['a', 'b', 'c', 'd'] + * + * // With limit + * split('a b c d', ' ', 2); + * // -> ['a', 'b'] + * + * // Backreferences in result array + * split('..word1 word2..', /([a-z]+)(\d+)/i); + * // -> ['..', 'word', '1', ' ', 'word', '2', '..'] + */ +module.exports = (function split(undef) { + + var nativeSplit = String.prototype.split, + compliantExecNpcg = /()??/.exec("")[1] === undef, + // NPCG: nonparticipating capturing group + self; + + self = function(str, separator, limit) { + // If `separator` is not a regex, use `nativeSplit` + if (Object.prototype.toString.call(separator) !== "[object RegExp]") { + return nativeSplit.call(str, separator, limit); + } + var output = [], + flags = (separator.ignoreCase ? "i" : "") + (separator.multiline ? "m" : "") + (separator.extended ? "x" : "") + // Proposed for ES6 + (separator.sticky ? "y" : ""), + // Firefox 3+ + lastLastIndex = 0, + // Make `global` and avoid `lastIndex` issues by working with a copy + separator = new RegExp(separator.source, flags + "g"), + separator2, match, lastIndex, lastLength; + str += ""; // Type-convert + if (!compliantExecNpcg) { + // Doesn't need flags gy, but they don't hurt + separator2 = new RegExp("^" + separator.source + "$(?!\\s)", flags); + } + /* Values for `limit`, per the spec: + * If undefined: 4294967295 // Math.pow(2, 32) - 1 + * If 0, Infinity, or NaN: 0 + * If positive number: limit = Math.floor(limit); if (limit > 4294967295) limit -= 4294967296; + * If negative number: 4294967296 - Math.floor(Math.abs(limit)) + * If other: Type-convert, then use the above rules + */ + limit = limit === undef ? -1 >>> 0 : // Math.pow(2, 32) - 1 + limit >>> 0; // ToUint32(limit) + while (match = separator.exec(str)) { + // `separator.lastIndex` is not reliable cross-browser + lastIndex = match.index + match[0].length; + if (lastIndex > lastLastIndex) { + output.push(str.slice(lastLastIndex, match.index)); + // Fix browsers whose `exec` methods don't consistently return `undefined` for + // nonparticipating capturing groups + if (!compliantExecNpcg && match.length > 1) { + match[0].replace(separator2, function() { + for (var i = 1; i < arguments.length - 2; i++) { + if (arguments[i] === undef) { + match[i] = undef; + } + } + }); + } + if (match.length > 1 && match.index < str.length) { + Array.prototype.push.apply(output, match.slice(1)); + } + lastLength = match[0].length; + lastLastIndex = lastIndex; + if (output.length >= limit) { + break; + } + } + if (separator.lastIndex === match.index) { + separator.lastIndex++; // Avoid an infinite loop + } + } + if (lastLastIndex === str.length) { + if (lastLength || !separator.test("")) { + output.push(""); + } + } else { + output.push(str.slice(lastLastIndex)); + } + return output.length > limit ? output.slice(0, limit) : output; + }; + + return self; +})(); + +},{}],5:[function(require,module,exports){ +'use strict'; + +var OneVersionConstraint = require('individual/one-version'); + +var MY_VERSION = '7'; +OneVersionConstraint('ev-store', MY_VERSION); + +var hashKey = '__EV_STORE_KEY@' + MY_VERSION; + +module.exports = EvStore; + +function EvStore(elem) { + var hash = elem[hashKey]; + + if (!hash) { + hash = elem[hashKey] = {}; + } + + return hash; +} + +},{"individual/one-version":8}],6:[function(require,module,exports){ +(function (global){ +var topLevel = typeof global !== 'undefined' ? global : + typeof window !== 'undefined' ? window : {} +var minDoc = require('min-document'); + +if (typeof document !== 'undefined') { + module.exports = document; +} else { + var doccy = topLevel['__GLOBAL_DOCUMENT_CACHE@4']; + + if (!doccy) { + doccy = topLevel['__GLOBAL_DOCUMENT_CACHE@4'] = minDoc; + } + + module.exports = doccy; +} + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{"min-document":3}],7:[function(require,module,exports){ +(function (global){ +'use strict'; + +/*global window, global*/ + +var root = typeof window !== 'undefined' ? + window : typeof global !== 'undefined' ? + global : {}; + +module.exports = Individual; + +function Individual(key, value) { + if (key in root) { + return root[key]; + } + + root[key] = value; + + return value; +} + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{}],8:[function(require,module,exports){ +'use strict'; + +var Individual = require('./index.js'); + +module.exports = OneVersion; + +function OneVersion(moduleName, version, defaultValue) { + var key = '__INDIVIDUAL_ONE_VERSION_' + moduleName; + var enforceKey = key + '_ENFORCE_SINGLETON'; + + var versionValue = Individual(enforceKey, version); + + if (versionValue !== version) { + throw new Error('Can only have one copy of ' + + moduleName + '.\n' + + 'You already have version ' + versionValue + + ' installed.\n' + + 'This means you cannot install version ' + version); + } + + return Individual(key, defaultValue); +} + +},{"./index.js":7}],9:[function(require,module,exports){ +"use strict"; + +module.exports = function isObject(x) { + return typeof x === "object" && x !== null; +}; + +},{}],10:[function(require,module,exports){ (function(){ function isValidEmail(v) { @@ -180,7 +428,7 @@ $('a.resend').on('click', function () { })(); -},{}],3:[function(require,module,exports){ +},{}],11:[function(require,module,exports){ (function (global){ /*! https://mths.be/punycode v1.4.0 by @mathias */ ;(function(root) { @@ -717,7 +965,7 @@ $('a.resend').on('click', function () { }(this)); }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) -},{}],4:[function(require,module,exports){ +},{}],12:[function(require,module,exports){ // Copyright Joyent, Inc. and other Node contributors. // // Permission is hereby granted, free of charge, to any person obtaining a @@ -803,7 +1051,7 @@ var isArray = Array.isArray || function (xs) { return Object.prototype.toString.call(xs) === '[object Array]'; }; -},{}],5:[function(require,module,exports){ +},{}],13:[function(require,module,exports){ // Copyright Joyent, Inc. and other Node contributors. // // Permission is hereby granted, free of charge, to any person obtaining a @@ -890,13 +1138,13 @@ var objectKeys = Object.keys || function (obj) { return res; }; -},{}],6:[function(require,module,exports){ +},{}],14:[function(require,module,exports){ 'use strict'; exports.decode = exports.parse = require('./decode'); exports.encode = exports.stringify = require('./encode'); -},{"./decode":4,"./encode":5}],7:[function(require,module,exports){ +},{"./decode":12,"./encode":13}],15:[function(require,module,exports){ // Copyright Joyent, Inc. and other Node contributors. // // Permission is hereby granted, free of charge, to any person obtaining a @@ -1630,7 +1878,7 @@ Url.prototype.parseHost = function() { if (host) this.hostname = host; }; -},{"./util":8,"punycode":3,"querystring":6}],8:[function(require,module,exports){ +},{"./util":16,"punycode":11,"querystring":14}],16:[function(require,module,exports){ 'use strict'; module.exports = { @@ -1648,4 +1896,1603 @@ module.exports = { } }; +},{}],17:[function(require,module,exports){ +(function(module) { + 'use strict'; + + module.exports.is_uri = is_iri; + module.exports.is_http_uri = is_http_iri; + module.exports.is_https_uri = is_https_iri; + module.exports.is_web_uri = is_web_iri; + // Create aliases + module.exports.isUri = is_iri; + module.exports.isHttpUri = is_http_iri; + module.exports.isHttpsUri = is_https_iri; + module.exports.isWebUri = is_web_iri; + + + // private function + // internal URI spitter method - direct from RFC 3986 + var splitUri = function(uri) { + var splitted = uri.match(/(?:([^:\/?#]+):)?(?:\/\/([^\/?#]*))?([^?#]*)(?:\?([^#]*))?(?:#(.*))?/); + return splitted; + }; + + function is_iri(value) { + if (!value) { + return; + } + + // check for illegal characters + if (/[^a-z0-9\:\/\?\#\[\]\@\!\$\&\'\(\)\*\+\,\;\=\.\-\_\~\%]/i.test(value)) return; + + // check for hex escapes that aren't complete + if (/%[^0-9a-f]/i.test(value)) return; + if (/%[0-9a-f](:?[^0-9a-f]|$)/i.test(value)) return; + + var splitted = []; + var scheme = ''; + var authority = ''; + var path = ''; + var query = ''; + var fragment = ''; + var out = ''; + + // from RFC 3986 + splitted = splitUri(value); + scheme = splitted[1]; + authority = splitted[2]; + path = splitted[3]; + query = splitted[4]; + fragment = splitted[5]; + + // scheme and path are required, though the path can be empty + if (!(scheme && scheme.length && path.length >= 0)) return; + + // if authority is present, the path must be empty or begin with a / + if (authority && authority.length) { + if (!(path.length === 0 || /^\//.test(path))) return; + } else { + // if authority is not present, the path must not start with // + if (/^\/\//.test(path)) return; + } + + // scheme must begin with a letter, then consist of letters, digits, +, ., or - + if (!/^[a-z][a-z0-9\+\-\.]*$/.test(scheme.toLowerCase())) return; + + // re-assemble the URL per section 5.3 in RFC 3986 + out += scheme + ':'; + if (authority && authority.length) { + out += '//' + authority; + } + + out += path; + + if (query && query.length) { + out += '?' + query; + } + + if (fragment && fragment.length) { + out += '#' + fragment; + } + + return out; + } + + function is_http_iri(value, allowHttps) { + if (!is_iri(value)) { + return; + } + + var splitted = []; + var scheme = ''; + var authority = ''; + var path = ''; + var port = ''; + var query = ''; + var fragment = ''; + var out = ''; + + // from RFC 3986 + splitted = splitUri(value); + scheme = splitted[1]; + authority = splitted[2]; + path = splitted[3]; + query = splitted[4]; + fragment = splitted[5]; + + if (!scheme) return; + + if(allowHttps) { + if (scheme.toLowerCase() != 'https') return; + } else { + if (scheme.toLowerCase() != 'http') return; + } + + // fully-qualified URIs must have an authority section that is + // a valid host + if (!authority) { + return; + } + + // enable port component + if (/:(\d+)$/.test(authority)) { + port = authority.match(/:(\d+)$/)[0]; + authority = authority.replace(/:\d+$/, ''); + } + + out += scheme + ':'; + out += '//' + authority; + + if (port) { + out += port; + } + + out += path; + + if(query && query.length){ + out += '?' + query; + } + + if(fragment && fragment.length){ + out += '#' + fragment; + } + + return out; + } + + function is_https_iri(value) { + return is_http_iri(value, true); + } + + function is_web_iri(value) { + return (is_http_iri(value) || is_https_iri(value)); + } + +})(module); + +},{}],18:[function(require,module,exports){ +var createElement = require("./vdom/create-element.js") + +module.exports = createElement + +},{"./vdom/create-element.js":23}],19:[function(require,module,exports){ +var diff = require("./vtree/diff.js") + +module.exports = diff + +},{"./vtree/diff.js":43}],20:[function(require,module,exports){ +var h = require("./virtual-hyperscript/index.js") + +module.exports = h + +},{"./virtual-hyperscript/index.js":30}],21:[function(require,module,exports){ +var patch = require("./vdom/patch.js") + +module.exports = patch + +},{"./vdom/patch.js":26}],22:[function(require,module,exports){ +var isObject = require("is-object") +var isHook = require("../vnode/is-vhook.js") + +module.exports = applyProperties + +function applyProperties(node, props, previous) { + for (var propName in props) { + var propValue = props[propName] + + if (propValue === undefined) { + removeProperty(node, propName, propValue, previous); + } else if (isHook(propValue)) { + removeProperty(node, propName, propValue, previous) + if (propValue.hook) { + propValue.hook(node, + propName, + previous ? previous[propName] : undefined) + } + } else { + if (isObject(propValue)) { + patchObject(node, props, previous, propName, propValue); + } else { + node[propName] = propValue + } + } + } +} + +function removeProperty(node, propName, propValue, previous) { + if (previous) { + var previousValue = previous[propName] + + if (!isHook(previousValue)) { + if (propName === "attributes") { + for (var attrName in previousValue) { + node.removeAttribute(attrName) + } + } else if (propName === "style") { + for (var i in previousValue) { + node.style[i] = "" + } + } else if (typeof previousValue === "string") { + node[propName] = "" + } else { + node[propName] = null + } + } else if (previousValue.unhook) { + previousValue.unhook(node, propName, propValue) + } + } +} + +function patchObject(node, props, previous, propName, propValue) { + var previousValue = previous ? previous[propName] : undefined + + // Set attributes + if (propName === "attributes") { + for (var attrName in propValue) { + var attrValue = propValue[attrName] + + if (attrValue === undefined) { + node.removeAttribute(attrName) + } else { + node.setAttribute(attrName, attrValue) + } + } + + return + } + + if(previousValue && isObject(previousValue) && + getPrototype(previousValue) !== getPrototype(propValue)) { + node[propName] = propValue + return + } + + if (!isObject(node[propName])) { + node[propName] = {} + } + + var replacer = propName === "style" ? "" : undefined + + for (var k in propValue) { + var value = propValue[k] + node[propName][k] = (value === undefined) ? replacer : value + } +} + +function getPrototype(value) { + if (Object.getPrototypeOf) { + return Object.getPrototypeOf(value) + } else if (value.__proto__) { + return value.__proto__ + } else if (value.constructor) { + return value.constructor.prototype + } +} + +},{"../vnode/is-vhook.js":34,"is-object":9}],23:[function(require,module,exports){ +var document = require("global/document") + +var applyProperties = require("./apply-properties") + +var isVNode = require("../vnode/is-vnode.js") +var isVText = require("../vnode/is-vtext.js") +var isWidget = require("../vnode/is-widget.js") +var handleThunk = require("../vnode/handle-thunk.js") + +module.exports = createElement + +function createElement(vnode, opts) { + var doc = opts ? opts.document || document : document + var warn = opts ? opts.warn : null + + vnode = handleThunk(vnode).a + + if (isWidget(vnode)) { + return vnode.init() + } else if (isVText(vnode)) { + return doc.createTextNode(vnode.text) + } else if (!isVNode(vnode)) { + if (warn) { + warn("Item is not a valid virtual dom node", vnode) + } + return null + } + + var node = (vnode.namespace === null) ? + doc.createElement(vnode.tagName) : + doc.createElementNS(vnode.namespace, vnode.tagName) + + var props = vnode.properties + applyProperties(node, props) + + var children = vnode.children + + for (var i = 0; i < children.length; i++) { + var childNode = createElement(children[i], opts) + if (childNode) { + node.appendChild(childNode) + } + } + + return node +} + +},{"../vnode/handle-thunk.js":32,"../vnode/is-vnode.js":35,"../vnode/is-vtext.js":36,"../vnode/is-widget.js":37,"./apply-properties":22,"global/document":6}],24:[function(require,module,exports){ +// Maps a virtual DOM tree onto a real DOM tree in an efficient manner. +// We don't want to read all of the DOM nodes in the tree so we use +// the in-order tree indexing to eliminate recursion down certain branches. +// We only recurse into a DOM node if we know that it contains a child of +// interest. + +var noChild = {} + +module.exports = domIndex + +function domIndex(rootNode, tree, indices, nodes) { + if (!indices || indices.length === 0) { + return {} + } else { + indices.sort(ascending) + return recurse(rootNode, tree, indices, nodes, 0) + } +} + +function recurse(rootNode, tree, indices, nodes, rootIndex) { + nodes = nodes || {} + + + if (rootNode) { + if (indexInRange(indices, rootIndex, rootIndex)) { + nodes[rootIndex] = rootNode + } + + var vChildren = tree.children + + if (vChildren) { + + var childNodes = rootNode.childNodes + + for (var i = 0; i < tree.children.length; i++) { + rootIndex += 1 + + var vChild = vChildren[i] || noChild + var nextIndex = rootIndex + (vChild.count || 0) + + // skip recursion down the tree if there are no nodes down here + if (indexInRange(indices, rootIndex, nextIndex)) { + recurse(childNodes[i], vChild, indices, nodes, rootIndex) + } + + rootIndex = nextIndex + } + } + } + + return nodes +} + +// Binary search for an index in the interval [left, right] +function indexInRange(indices, left, right) { + if (indices.length === 0) { + return false + } + + var minIndex = 0 + var maxIndex = indices.length - 1 + var currentIndex + var currentItem + + while (minIndex <= maxIndex) { + currentIndex = ((maxIndex + minIndex) / 2) >> 0 + currentItem = indices[currentIndex] + + if (minIndex === maxIndex) { + return currentItem >= left && currentItem <= right + } else if (currentItem < left) { + minIndex = currentIndex + 1 + } else if (currentItem > right) { + maxIndex = currentIndex - 1 + } else { + return true + } + } + + return false; +} + +function ascending(a, b) { + return a > b ? 1 : -1 +} + +},{}],25:[function(require,module,exports){ +var applyProperties = require("./apply-properties") + +var isWidget = require("../vnode/is-widget.js") +var VPatch = require("../vnode/vpatch.js") + +var updateWidget = require("./update-widget") + +module.exports = applyPatch + +function applyPatch(vpatch, domNode, renderOptions) { + var type = vpatch.type + var vNode = vpatch.vNode + var patch = vpatch.patch + + switch (type) { + case VPatch.REMOVE: + return removeNode(domNode, vNode) + case VPatch.INSERT: + return insertNode(domNode, patch, renderOptions) + case VPatch.VTEXT: + return stringPatch(domNode, vNode, patch, renderOptions) + case VPatch.WIDGET: + return widgetPatch(domNode, vNode, patch, renderOptions) + case VPatch.VNODE: + return vNodePatch(domNode, vNode, patch, renderOptions) + case VPatch.ORDER: + reorderChildren(domNode, patch) + return domNode + case VPatch.PROPS: + applyProperties(domNode, patch, vNode.properties) + return domNode + case VPatch.THUNK: + return replaceRoot(domNode, + renderOptions.patch(domNode, patch, renderOptions)) + default: + return domNode + } +} + +function removeNode(domNode, vNode) { + var parentNode = domNode.parentNode + + if (parentNode) { + parentNode.removeChild(domNode) + } + + destroyWidget(domNode, vNode); + + return null +} + +function insertNode(parentNode, vNode, renderOptions) { + var newNode = renderOptions.render(vNode, renderOptions) + + if (parentNode) { + parentNode.appendChild(newNode) + } + + return parentNode +} + +function stringPatch(domNode, leftVNode, vText, renderOptions) { + var newNode + + if (domNode.nodeType === 3) { + domNode.replaceData(0, domNode.length, vText.text) + newNode = domNode + } else { + var parentNode = domNode.parentNode + newNode = renderOptions.render(vText, renderOptions) + + if (parentNode && newNode !== domNode) { + parentNode.replaceChild(newNode, domNode) + } + } + + return newNode +} + +function widgetPatch(domNode, leftVNode, widget, renderOptions) { + var updating = updateWidget(leftVNode, widget) + var newNode + + if (updating) { + newNode = widget.update(leftVNode, domNode) || domNode + } else { + newNode = renderOptions.render(widget, renderOptions) + } + + var parentNode = domNode.parentNode + + if (parentNode && newNode !== domNode) { + parentNode.replaceChild(newNode, domNode) + } + + if (!updating) { + destroyWidget(domNode, leftVNode) + } + + return newNode +} + +function vNodePatch(domNode, leftVNode, vNode, renderOptions) { + var parentNode = domNode.parentNode + var newNode = renderOptions.render(vNode, renderOptions) + + if (parentNode && newNode !== domNode) { + parentNode.replaceChild(newNode, domNode) + } + + return newNode +} + +function destroyWidget(domNode, w) { + if (typeof w.destroy === "function" && isWidget(w)) { + w.destroy(domNode) + } +} + +function reorderChildren(domNode, moves) { + var childNodes = domNode.childNodes + var keyMap = {} + var node + var remove + var insert + + for (var i = 0; i < moves.removes.length; i++) { + remove = moves.removes[i] + node = childNodes[remove.from] + if (remove.key) { + keyMap[remove.key] = node + } + domNode.removeChild(node) + } + + var length = childNodes.length + for (var j = 0; j < moves.inserts.length; j++) { + insert = moves.inserts[j] + node = keyMap[insert.key] + // this is the weirdest bug i've ever seen in webkit + domNode.insertBefore(node, insert.to >= length++ ? null : childNodes[insert.to]) + } +} + +function replaceRoot(oldRoot, newRoot) { + if (oldRoot && newRoot && oldRoot !== newRoot && oldRoot.parentNode) { + oldRoot.parentNode.replaceChild(newRoot, oldRoot) + } + + return newRoot; +} + +},{"../vnode/is-widget.js":37,"../vnode/vpatch.js":40,"./apply-properties":22,"./update-widget":27}],26:[function(require,module,exports){ +var document = require("global/document") +var isArray = require("x-is-array") + +var render = require("./create-element") +var domIndex = require("./dom-index") +var patchOp = require("./patch-op") +module.exports = patch + +function patch(rootNode, patches, renderOptions) { + renderOptions = renderOptions || {} + renderOptions.patch = renderOptions.patch && renderOptions.patch !== patch + ? renderOptions.patch + : patchRecursive + renderOptions.render = renderOptions.render || render + + return renderOptions.patch(rootNode, patches, renderOptions) +} + +function patchRecursive(rootNode, patches, renderOptions) { + var indices = patchIndices(patches) + + if (indices.length === 0) { + return rootNode + } + + var index = domIndex(rootNode, patches.a, indices) + var ownerDocument = rootNode.ownerDocument + + if (!renderOptions.document && ownerDocument !== document) { + renderOptions.document = ownerDocument + } + + for (var i = 0; i < indices.length; i++) { + var nodeIndex = indices[i] + rootNode = applyPatch(rootNode, + index[nodeIndex], + patches[nodeIndex], + renderOptions) + } + + return rootNode +} + +function applyPatch(rootNode, domNode, patchList, renderOptions) { + if (!domNode) { + return rootNode + } + + var newNode + + if (isArray(patchList)) { + for (var i = 0; i < patchList.length; i++) { + newNode = patchOp(patchList[i], domNode, renderOptions) + + if (domNode === rootNode) { + rootNode = newNode + } + } + } else { + newNode = patchOp(patchList, domNode, renderOptions) + + if (domNode === rootNode) { + rootNode = newNode + } + } + + return rootNode +} + +function patchIndices(patches) { + var indices = [] + + for (var key in patches) { + if (key !== "a") { + indices.push(Number(key)) + } + } + + return indices +} + +},{"./create-element":23,"./dom-index":24,"./patch-op":25,"global/document":6,"x-is-array":44}],27:[function(require,module,exports){ +var isWidget = require("../vnode/is-widget.js") + +module.exports = updateWidget + +function updateWidget(a, b) { + if (isWidget(a) && isWidget(b)) { + if ("name" in a && "name" in b) { + return a.id === b.id + } else { + return a.init === b.init + } + } + + return false +} + +},{"../vnode/is-widget.js":37}],28:[function(require,module,exports){ +'use strict'; + +var EvStore = require('ev-store'); + +module.exports = EvHook; + +function EvHook(value) { + if (!(this instanceof EvHook)) { + return new EvHook(value); + } + + this.value = value; +} + +EvHook.prototype.hook = function (node, propertyName) { + var es = EvStore(node); + var propName = propertyName.substr(3); + + es[propName] = this.value; +}; + +EvHook.prototype.unhook = function(node, propertyName) { + var es = EvStore(node); + var propName = propertyName.substr(3); + + es[propName] = undefined; +}; + +},{"ev-store":5}],29:[function(require,module,exports){ +'use strict'; + +module.exports = SoftSetHook; + +function SoftSetHook(value) { + if (!(this instanceof SoftSetHook)) { + return new SoftSetHook(value); + } + + this.value = value; +} + +SoftSetHook.prototype.hook = function (node, propertyName) { + if (node[propertyName] !== this.value) { + node[propertyName] = this.value; + } +}; + +},{}],30:[function(require,module,exports){ +'use strict'; + +var isArray = require('x-is-array'); + +var VNode = require('../vnode/vnode.js'); +var VText = require('../vnode/vtext.js'); +var isVNode = require('../vnode/is-vnode'); +var isVText = require('../vnode/is-vtext'); +var isWidget = require('../vnode/is-widget'); +var isHook = require('../vnode/is-vhook'); +var isVThunk = require('../vnode/is-thunk'); + +var parseTag = require('./parse-tag.js'); +var softSetHook = require('./hooks/soft-set-hook.js'); +var evHook = require('./hooks/ev-hook.js'); + +module.exports = h; + +function h(tagName, properties, children) { + var childNodes = []; + var tag, props, key, namespace; + + if (!children && isChildren(properties)) { + children = properties; + props = {}; + } + + props = props || properties || {}; + tag = parseTag(tagName, props); + + // support keys + if (props.hasOwnProperty('key')) { + key = props.key; + props.key = undefined; + } + + // support namespace + if (props.hasOwnProperty('namespace')) { + namespace = props.namespace; + props.namespace = undefined; + } + + // fix cursor bug + if (tag === 'INPUT' && + !namespace && + props.hasOwnProperty('value') && + props.value !== undefined && + !isHook(props.value) + ) { + props.value = softSetHook(props.value); + } + + transformProperties(props); + + if (children !== undefined && children !== null) { + addChild(children, childNodes, tag, props); + } + + + return new VNode(tag, props, childNodes, key, namespace); +} + +function addChild(c, childNodes, tag, props) { + if (typeof c === 'string') { + childNodes.push(new VText(c)); + } else if (typeof c === 'number') { + childNodes.push(new VText(String(c))); + } else if (isChild(c)) { + childNodes.push(c); + } else if (isArray(c)) { + for (var i = 0; i < c.length; i++) { + addChild(c[i], childNodes, tag, props); + } + } else if (c === null || c === undefined) { + return; + } else { + throw UnexpectedVirtualElement({ + foreignObject: c, + parentVnode: { + tagName: tag, + properties: props + } + }); + } +} + +function transformProperties(props) { + for (var propName in props) { + if (props.hasOwnProperty(propName)) { + var value = props[propName]; + + if (isHook(value)) { + continue; + } + + if (propName.substr(0, 3) === 'ev-') { + // add ev-foo support + props[propName] = evHook(value); + } + } + } +} + +function isChild(x) { + return isVNode(x) || isVText(x) || isWidget(x) || isVThunk(x); +} + +function isChildren(x) { + return typeof x === 'string' || isArray(x) || isChild(x); +} + +function UnexpectedVirtualElement(data) { + var err = new Error(); + + err.type = 'virtual-hyperscript.unexpected.virtual-element'; + err.message = 'Unexpected virtual child passed to h().\n' + + 'Expected a VNode / Vthunk / VWidget / string but:\n' + + 'got:\n' + + errorString(data.foreignObject) + + '.\n' + + 'The parent vnode is:\n' + + errorString(data.parentVnode) + '\n' + + 'Suggested fix: change your `h(..., [ ... ])` callsite.'; + err.foreignObject = data.foreignObject; + err.parentVnode = data.parentVnode; + + return err; +} + +function errorString(obj) { + try { + return JSON.stringify(obj, null, ' '); + } catch (e) { + return String(obj); + } +} + +},{"../vnode/is-thunk":33,"../vnode/is-vhook":34,"../vnode/is-vnode":35,"../vnode/is-vtext":36,"../vnode/is-widget":37,"../vnode/vnode.js":39,"../vnode/vtext.js":41,"./hooks/ev-hook.js":28,"./hooks/soft-set-hook.js":29,"./parse-tag.js":31,"x-is-array":44}],31:[function(require,module,exports){ +'use strict'; + +var split = require('browser-split'); + +var classIdSplit = /([\.#]?[a-zA-Z0-9\u007F-\uFFFF_:-]+)/; +var notClassId = /^\.|#/; + +module.exports = parseTag; + +function parseTag(tag, props) { + if (!tag) { + return 'DIV'; + } + + var noId = !(props.hasOwnProperty('id')); + + var tagParts = split(tag, classIdSplit); + var tagName = null; + + if (notClassId.test(tagParts[1])) { + tagName = 'DIV'; + } + + var classes, part, type, i; + + for (i = 0; i < tagParts.length; i++) { + part = tagParts[i]; + + if (!part) { + continue; + } + + type = part.charAt(0); + + if (!tagName) { + tagName = part; + } else if (type === '.') { + classes = classes || []; + classes.push(part.substring(1, part.length)); + } else if (type === '#' && noId) { + props.id = part.substring(1, part.length); + } + } + + if (classes) { + if (props.className) { + classes.push(props.className); + } + + props.className = classes.join(' '); + } + + return props.namespace ? tagName : tagName.toUpperCase(); +} + +},{"browser-split":4}],32:[function(require,module,exports){ +var isVNode = require("./is-vnode") +var isVText = require("./is-vtext") +var isWidget = require("./is-widget") +var isThunk = require("./is-thunk") + +module.exports = handleThunk + +function handleThunk(a, b) { + var renderedA = a + var renderedB = b + + if (isThunk(b)) { + renderedB = renderThunk(b, a) + } + + if (isThunk(a)) { + renderedA = renderThunk(a, null) + } + + return { + a: renderedA, + b: renderedB + } +} + +function renderThunk(thunk, previous) { + var renderedThunk = thunk.vnode + + if (!renderedThunk) { + renderedThunk = thunk.vnode = thunk.render(previous) + } + + if (!(isVNode(renderedThunk) || + isVText(renderedThunk) || + isWidget(renderedThunk))) { + throw new Error("thunk did not return a valid node"); + } + + return renderedThunk +} + +},{"./is-thunk":33,"./is-vnode":35,"./is-vtext":36,"./is-widget":37}],33:[function(require,module,exports){ +module.exports = isThunk + +function isThunk(t) { + return t && t.type === "Thunk" +} + +},{}],34:[function(require,module,exports){ +module.exports = isHook + +function isHook(hook) { + return hook && + (typeof hook.hook === "function" && !hook.hasOwnProperty("hook") || + typeof hook.unhook === "function" && !hook.hasOwnProperty("unhook")) +} + +},{}],35:[function(require,module,exports){ +var version = require("./version") + +module.exports = isVirtualNode + +function isVirtualNode(x) { + return x && x.type === "VirtualNode" && x.version === version +} + +},{"./version":38}],36:[function(require,module,exports){ +var version = require("./version") + +module.exports = isVirtualText + +function isVirtualText(x) { + return x && x.type === "VirtualText" && x.version === version +} + +},{"./version":38}],37:[function(require,module,exports){ +module.exports = isWidget + +function isWidget(w) { + return w && w.type === "Widget" +} + +},{}],38:[function(require,module,exports){ +module.exports = "2" + +},{}],39:[function(require,module,exports){ +var version = require("./version") +var isVNode = require("./is-vnode") +var isWidget = require("./is-widget") +var isThunk = require("./is-thunk") +var isVHook = require("./is-vhook") + +module.exports = VirtualNode + +var noProperties = {} +var noChildren = [] + +function VirtualNode(tagName, properties, children, key, namespace) { + this.tagName = tagName + this.properties = properties || noProperties + this.children = children || noChildren + this.key = key != null ? String(key) : undefined + this.namespace = (typeof namespace === "string") ? namespace : null + + var count = (children && children.length) || 0 + var descendants = 0 + var hasWidgets = false + var hasThunks = false + var descendantHooks = false + var hooks + + for (var propName in properties) { + if (properties.hasOwnProperty(propName)) { + var property = properties[propName] + if (isVHook(property) && property.unhook) { + if (!hooks) { + hooks = {} + } + + hooks[propName] = property + } + } + } + + for (var i = 0; i < count; i++) { + var child = children[i] + if (isVNode(child)) { + descendants += child.count || 0 + + if (!hasWidgets && child.hasWidgets) { + hasWidgets = true + } + + if (!hasThunks && child.hasThunks) { + hasThunks = true + } + + if (!descendantHooks && (child.hooks || child.descendantHooks)) { + descendantHooks = true + } + } else if (!hasWidgets && isWidget(child)) { + if (typeof child.destroy === "function") { + hasWidgets = true + } + } else if (!hasThunks && isThunk(child)) { + hasThunks = true; + } + } + + this.count = count + descendants + this.hasWidgets = hasWidgets + this.hasThunks = hasThunks + this.hooks = hooks + this.descendantHooks = descendantHooks +} + +VirtualNode.prototype.version = version +VirtualNode.prototype.type = "VirtualNode" + +},{"./is-thunk":33,"./is-vhook":34,"./is-vnode":35,"./is-widget":37,"./version":38}],40:[function(require,module,exports){ +var version = require("./version") + +VirtualPatch.NONE = 0 +VirtualPatch.VTEXT = 1 +VirtualPatch.VNODE = 2 +VirtualPatch.WIDGET = 3 +VirtualPatch.PROPS = 4 +VirtualPatch.ORDER = 5 +VirtualPatch.INSERT = 6 +VirtualPatch.REMOVE = 7 +VirtualPatch.THUNK = 8 + +module.exports = VirtualPatch + +function VirtualPatch(type, vNode, patch) { + this.type = Number(type) + this.vNode = vNode + this.patch = patch +} + +VirtualPatch.prototype.version = version +VirtualPatch.prototype.type = "VirtualPatch" + +},{"./version":38}],41:[function(require,module,exports){ +var version = require("./version") + +module.exports = VirtualText + +function VirtualText(text) { + this.text = String(text) +} + +VirtualText.prototype.version = version +VirtualText.prototype.type = "VirtualText" + +},{"./version":38}],42:[function(require,module,exports){ +var isObject = require("is-object") +var isHook = require("../vnode/is-vhook") + +module.exports = diffProps + +function diffProps(a, b) { + var diff + + for (var aKey in a) { + if (!(aKey in b)) { + diff = diff || {} + diff[aKey] = undefined + } + + var aValue = a[aKey] + var bValue = b[aKey] + + if (aValue === bValue) { + continue + } else if (isObject(aValue) && isObject(bValue)) { + if (getPrototype(bValue) !== getPrototype(aValue)) { + diff = diff || {} + diff[aKey] = bValue + } else if (isHook(bValue)) { + diff = diff || {} + diff[aKey] = bValue + } else { + var objectDiff = diffProps(aValue, bValue) + if (objectDiff) { + diff = diff || {} + diff[aKey] = objectDiff + } + } + } else { + diff = diff || {} + diff[aKey] = bValue + } + } + + for (var bKey in b) { + if (!(bKey in a)) { + diff = diff || {} + diff[bKey] = b[bKey] + } + } + + return diff +} + +function getPrototype(value) { + if (Object.getPrototypeOf) { + return Object.getPrototypeOf(value) + } else if (value.__proto__) { + return value.__proto__ + } else if (value.constructor) { + return value.constructor.prototype + } +} + +},{"../vnode/is-vhook":34,"is-object":9}],43:[function(require,module,exports){ +var isArray = require("x-is-array") + +var VPatch = require("../vnode/vpatch") +var isVNode = require("../vnode/is-vnode") +var isVText = require("../vnode/is-vtext") +var isWidget = require("../vnode/is-widget") +var isThunk = require("../vnode/is-thunk") +var handleThunk = require("../vnode/handle-thunk") + +var diffProps = require("./diff-props") + +module.exports = diff + +function diff(a, b) { + var patch = { a: a } + walk(a, b, patch, 0) + return patch +} + +function walk(a, b, patch, index) { + if (a === b) { + return + } + + var apply = patch[index] + var applyClear = false + + if (isThunk(a) || isThunk(b)) { + thunks(a, b, patch, index) + } else if (b == null) { + + // If a is a widget we will add a remove patch for it + // Otherwise any child widgets/hooks must be destroyed. + // This prevents adding two remove patches for a widget. + if (!isWidget(a)) { + clearState(a, patch, index) + apply = patch[index] + } + + apply = appendPatch(apply, new VPatch(VPatch.REMOVE, a, b)) + } else if (isVNode(b)) { + if (isVNode(a)) { + if (a.tagName === b.tagName && + a.namespace === b.namespace && + a.key === b.key) { + var propsPatch = diffProps(a.properties, b.properties) + if (propsPatch) { + apply = appendPatch(apply, + new VPatch(VPatch.PROPS, a, propsPatch)) + } + apply = diffChildren(a, b, patch, apply, index) + } else { + apply = appendPatch(apply, new VPatch(VPatch.VNODE, a, b)) + applyClear = true + } + } else { + apply = appendPatch(apply, new VPatch(VPatch.VNODE, a, b)) + applyClear = true + } + } else if (isVText(b)) { + if (!isVText(a)) { + apply = appendPatch(apply, new VPatch(VPatch.VTEXT, a, b)) + applyClear = true + } else if (a.text !== b.text) { + apply = appendPatch(apply, new VPatch(VPatch.VTEXT, a, b)) + } + } else if (isWidget(b)) { + if (!isWidget(a)) { + applyClear = true + } + + apply = appendPatch(apply, new VPatch(VPatch.WIDGET, a, b)) + } + + if (apply) { + patch[index] = apply + } + + if (applyClear) { + clearState(a, patch, index) + } +} + +function diffChildren(a, b, patch, apply, index) { + var aChildren = a.children + var orderedSet = reorder(aChildren, b.children) + var bChildren = orderedSet.children + + var aLen = aChildren.length + var bLen = bChildren.length + var len = aLen > bLen ? aLen : bLen + + for (var i = 0; i < len; i++) { + var leftNode = aChildren[i] + var rightNode = bChildren[i] + index += 1 + + if (!leftNode) { + if (rightNode) { + // Excess nodes in b need to be added + apply = appendPatch(apply, + new VPatch(VPatch.INSERT, null, rightNode)) + } + } else { + walk(leftNode, rightNode, patch, index) + } + + if (isVNode(leftNode) && leftNode.count) { + index += leftNode.count + } + } + + if (orderedSet.moves) { + // Reorder nodes last + apply = appendPatch(apply, new VPatch( + VPatch.ORDER, + a, + orderedSet.moves + )) + } + + return apply +} + +function clearState(vNode, patch, index) { + // TODO: Make this a single walk, not two + unhook(vNode, patch, index) + destroyWidgets(vNode, patch, index) +} + +// Patch records for all destroyed widgets must be added because we need +// a DOM node reference for the destroy function +function destroyWidgets(vNode, patch, index) { + if (isWidget(vNode)) { + if (typeof vNode.destroy === "function") { + patch[index] = appendPatch( + patch[index], + new VPatch(VPatch.REMOVE, vNode, null) + ) + } + } else if (isVNode(vNode) && (vNode.hasWidgets || vNode.hasThunks)) { + var children = vNode.children + var len = children.length + for (var i = 0; i < len; i++) { + var child = children[i] + index += 1 + + destroyWidgets(child, patch, index) + + if (isVNode(child) && child.count) { + index += child.count + } + } + } else if (isThunk(vNode)) { + thunks(vNode, null, patch, index) + } +} + +// Create a sub-patch for thunks +function thunks(a, b, patch, index) { + var nodes = handleThunk(a, b) + var thunkPatch = diff(nodes.a, nodes.b) + if (hasPatches(thunkPatch)) { + patch[index] = new VPatch(VPatch.THUNK, null, thunkPatch) + } +} + +function hasPatches(patch) { + for (var index in patch) { + if (index !== "a") { + return true + } + } + + return false +} + +// Execute hooks when two nodes are identical +function unhook(vNode, patch, index) { + if (isVNode(vNode)) { + if (vNode.hooks) { + patch[index] = appendPatch( + patch[index], + new VPatch( + VPatch.PROPS, + vNode, + undefinedKeys(vNode.hooks) + ) + ) + } + + if (vNode.descendantHooks || vNode.hasThunks) { + var children = vNode.children + var len = children.length + for (var i = 0; i < len; i++) { + var child = children[i] + index += 1 + + unhook(child, patch, index) + + if (isVNode(child) && child.count) { + index += child.count + } + } + } + } else if (isThunk(vNode)) { + thunks(vNode, null, patch, index) + } +} + +function undefinedKeys(obj) { + var result = {} + + for (var key in obj) { + result[key] = undefined + } + + return result +} + +// List diff, naive left to right reordering +function reorder(aChildren, bChildren) { + // O(M) time, O(M) memory + var bChildIndex = keyIndex(bChildren) + var bKeys = bChildIndex.keys + var bFree = bChildIndex.free + + if (bFree.length === bChildren.length) { + return { + children: bChildren, + moves: null + } + } + + // O(N) time, O(N) memory + var aChildIndex = keyIndex(aChildren) + var aKeys = aChildIndex.keys + var aFree = aChildIndex.free + + if (aFree.length === aChildren.length) { + return { + children: bChildren, + moves: null + } + } + + // O(MAX(N, M)) memory + var newChildren = [] + + var freeIndex = 0 + var freeCount = bFree.length + var deletedItems = 0 + + // Iterate through a and match a node in b + // O(N) time, + for (var i = 0 ; i < aChildren.length; i++) { + var aItem = aChildren[i] + var itemIndex + + if (aItem.key) { + if (bKeys.hasOwnProperty(aItem.key)) { + // Match up the old keys + itemIndex = bKeys[aItem.key] + newChildren.push(bChildren[itemIndex]) + + } else { + // Remove old keyed items + itemIndex = i - deletedItems++ + newChildren.push(null) + } + } else { + // Match the item in a with the next free item in b + if (freeIndex < freeCount) { + itemIndex = bFree[freeIndex++] + newChildren.push(bChildren[itemIndex]) + } else { + // There are no free items in b to match with + // the free items in a, so the extra free nodes + // are deleted. + itemIndex = i - deletedItems++ + newChildren.push(null) + } + } + } + + var lastFreeIndex = freeIndex >= bFree.length ? + bChildren.length : + bFree[freeIndex] + + // Iterate through b and append any new keys + // O(M) time + for (var j = 0; j < bChildren.length; j++) { + var newItem = bChildren[j] + + if (newItem.key) { + if (!aKeys.hasOwnProperty(newItem.key)) { + // Add any new keyed items + // We are adding new items to the end and then sorting them + // in place. In future we should insert new items in place. + newChildren.push(newItem) + } + } else if (j >= lastFreeIndex) { + // Add any leftover non-keyed items + newChildren.push(newItem) + } + } + + var simulate = newChildren.slice() + var simulateIndex = 0 + var removes = [] + var inserts = [] + var simulateItem + + for (var k = 0; k < bChildren.length;) { + var wantedItem = bChildren[k] + simulateItem = simulate[simulateIndex] + + // remove items + while (simulateItem === null && simulate.length) { + removes.push(remove(simulate, simulateIndex, null)) + simulateItem = simulate[simulateIndex] + } + + if (!simulateItem || simulateItem.key !== wantedItem.key) { + // if we need a key in this position... + if (wantedItem.key) { + if (simulateItem && simulateItem.key) { + // if an insert doesn't put this key in place, it needs to move + if (bKeys[simulateItem.key] !== k + 1) { + removes.push(remove(simulate, simulateIndex, simulateItem.key)) + simulateItem = simulate[simulateIndex] + // if the remove didn't put the wanted item in place, we need to insert it + if (!simulateItem || simulateItem.key !== wantedItem.key) { + inserts.push({key: wantedItem.key, to: k}) + } + // items are matching, so skip ahead + else { + simulateIndex++ + } + } + else { + inserts.push({key: wantedItem.key, to: k}) + } + } + else { + inserts.push({key: wantedItem.key, to: k}) + } + k++ + } + // a key in simulate has no matching wanted key, remove it + else if (simulateItem && simulateItem.key) { + removes.push(remove(simulate, simulateIndex, simulateItem.key)) + } + } + else { + simulateIndex++ + k++ + } + } + + // remove all the remaining nodes from simulate + while(simulateIndex < simulate.length) { + simulateItem = simulate[simulateIndex] + removes.push(remove(simulate, simulateIndex, simulateItem && simulateItem.key)) + } + + // If the only moves we have are deletes then we can just + // let the delete patch remove these items. + if (removes.length === deletedItems && !inserts.length) { + return { + children: newChildren, + moves: null + } + } + + return { + children: newChildren, + moves: { + removes: removes, + inserts: inserts + } + } +} + +function remove(arr, index, key) { + arr.splice(index, 1) + + return { + from: index, + key: key + } +} + +function keyIndex(children) { + var keys = {} + var free = [] + var length = children.length + + for (var i = 0; i < length; i++) { + var child = children[i] + + if (child.key) { + keys[child.key] = i + } else { + free.push(i) + } + } + + return { + keys: keys, // A hash of key name to index + free: free // An array of unkeyed item indices + } +} + +function appendPatch(apply, patch) { + if (apply) { + if (isArray(apply)) { + apply.push(patch) + } else { + apply = [apply, patch] + } + + return apply + } else { + return patch + } +} + +},{"../vnode/handle-thunk":32,"../vnode/is-thunk":33,"../vnode/is-vnode":35,"../vnode/is-vtext":36,"../vnode/is-widget":37,"../vnode/vpatch":40,"./diff-props":42,"x-is-array":44}],44:[function(require,module,exports){ +var nativeIsArray = Array.isArray +var toString = Object.prototype.toString + +module.exports = nativeIsArray || isArray + +function isArray(obj) { + return toString.call(obj) === "[object Array]" +} + },{}]},{},[1]); diff --git a/formspree/static/js/main.js b/formspree/static/js/main.js index 392bb340..2b34be5c 100644 --- a/formspree/static/js/main.js +++ b/formspree/static/js/main.js @@ -1,6 +1,3 @@ -const url = require('url') -const isValidEmail = require('is-valid-email') - const $ = window.$ const StripeCheckout = window.StripeCheckout const toastr = window.toastr @@ -65,70 +62,6 @@ function modals () { } modals() -/* create-form validation for site-wide forms */ -function sitewide () { - let createform = $('#create-form') - let emailInput = createform.find('input[name="email"]') - let urlInput = createform.find('input[name="url"]') - let checkbox = createform.find('input[name="sitewide"]') - let verifyButton = createform.find('.verify-button') - let createButton = createform.find('.create-button') - let info = createform.find('.verify-info') - - checkbox.on('change', run) - emailInput.on('input', run) - urlInput.on('input', run) - - function run () { - if (checkbox.is(':checked')) { - let email = emailInput.val().trim() - let urlp = url.parse(urlInput.val().trim()) - - if (isValidEmail(email) && urlp.host) { - let sitewideFile = `formspree_verify_${email}.txt` - verifyButton.css('visibility', 'visible') - info.html(`Please ensure ${url.resolve(urlInput.val(), '/' + sitewideFile)} exists`) - } else { - // wrong input - if (!urlp.host) { // invalid url - info.text('Please input a valid URL.') - } else { // invalid email - info.text('Please input a valid email address.') - } - } - - createButton.find('button').prop('disabled', true) - info.css('visibility', 'visible') - } else { - // toggle sitewide off - info.css('visibility', 'hidden') - verifyButton.css('visibility', 'hidden') - createButton.css('visibility', 'visible') - } - } - - verifyButton.find('button').on('click', function () { - $.ajax({ - url: '/forms/sitewide-check?' + createform.find('form').serialize(), - success: function () { - toastr.success('The file exists! you can create your site-wide form now.') - createButton.find('button').prop('disabled', false) - verifyButton.css('visibility', 'hidden') - info.css('visibility', 'hidden') - }, - error: function () { - toastr.warning("The verification file wasn't found.") - verifyButton.find('button').prop('disabled', true) - setTimeout(() => { - verifyButton.find('button').prop('disabled', false) - }, 5000) - } - }) - return false - }) -} -sitewide() - /* turning flask flash messages into js popup notifications */ window.popupMessages.forEach(function (m, i) { var category = m[0] || 'info' @@ -158,3 +91,6 @@ $('a.resend').on('click', function () { $(this).hide() $('form.resend').show() }) + +/* scripts at other files */ +require('./sitewide')() diff --git a/formspree/static/js/sitewide.js b/formspree/static/js/sitewide.js new file mode 100644 index 00000000..619c837e --- /dev/null +++ b/formspree/static/js/sitewide.js @@ -0,0 +1,147 @@ +const url = require('url') +const isValidUrl = require('valid-url').isWebUri +const isValidEmail = require('is-valid-email') + +const h = require('virtual-dom/h') +const diff = require('virtual-dom/diff') +const patch = require('virtual-dom/patch') +const createElement = require('virtual-dom/create-element') + +const $ = window.$ +const toastr = window.toastr + +/* create-form validation for site-wide forms */ +module.exports = function sitewide () { + var parentNode = $('#create-form .container') + + let formActionURL = parentNode.find('form').attr('action') + let currentUserEmail = parentNode.find('[name="email"]').val() + + // since we have javascript, let's trash this HTML and recreate with virtual-dom + parentNode.html('') + + var data = { + invalid: null, + sitewide: false, + verified: false, + email: currentUserEmail + } + var tree = render(data) + var rootNode = createElement(tree) + parentNode[0].appendChild(rootNode) + + parentNode.on('change', 'input[name="sitewide"]', run) + parentNode.on('input', 'input[name="url"], input[name="email"]', run) + parentNode.on('click', '.verify button', check) + + function run () { + let checkbox = parentNode.find('input[name="sitewide"]') + + let email = parentNode.find('input[name="email"]').val().trim() + let urlv = parentNode.find('input[name="url"]').val().trim() + let sitewide = checkbox.is(':checked') + + // wrong input + if (!isValidEmail(email)) { // invalid email + data.invalid = 'email' + } else if (sitewide && !isValidUrl(urlv)) { // invalid url with sitewide + data.invalid = 'url' + } else if (!sitewide && urlv && !isValidUrl(urlv)) { // invalid url without sitewide + data.invalid = 'url' + } else { + data.invalid = null + } + + data.sitewide = sitewide + data.urlv = urlv + data.email = email + + apply(render(data)) + } + + function check () { + $.ajax({ + url: '/forms/sitewide-check?' + parentNode.find('form').serialize(), + success: function () { + toastr.success('The file exists! you can create your site-wide form now.') + data.verified = true + apply(render(data)) + }, + error: function () { + toastr.warning("The verification file wasn't found.") + data.verified = false + data.disableVerification = true + apply(render(data)) + + setTimeout(() => { + data.disableVerification = false + apply(render(data)) + }, 5000) + } + }) + + return false + } + + function apply (vtree) { + let patches = diff(tree, vtree) + rootNode = patch(rootNode, patches) + tree = vtree + } + + function render ({invalid, sitewide, verified, urlv, email, disableVerification}) { + return h('form', {method: 'post', action: formActionURL}, [ + h('.col-1-1', [ + h('h4', 'Send email to:'), + h('input', {type: 'email', name: 'email', placeholder: 'You can point this form to any email address', value: email}) + ]), + h('.col-1-1', [ + h('h4', 'From URL:'), + h('input', {type: 'url', name: 'url', placeholder: 'Leave blank to send confirmation email when first submitted'}) + ]), + h('.container', [ + h('.col-1-4', [ + h('label.hint--bottom', {dataset: {hint: 'A site-wide form is a form that you can place on all pages of your website -- and you just have to confirm once!'}}, [ + h('input', {type: 'checkbox', name: 'sitewide', value: 'true'}), + ' site-wide' + ]) + ]), + h('.col-3-4', [ + invalid + ? h('div.red', invalid === 'email' + ? 'Please input a valid email address.' + : [ + 'Please input a valid URL. For example: ', + h('span.code', url.resolve('http://www.mywebsite.com', sitewide ? '' : '/contact.html')) + ]) + : sitewide && verified || !sitewide + ? h('div', {innerHTML: '​'}) + : h('span', [ + 'Please ensure ', + h('span.code', url.resolve(urlv, '/formspree-verify.txt')), + ' exists and contains a line with ', + h('span.code', email) + ]) + ]), + h('.col-1-3', [ + h('.verify', [ + h('button', sitewide && !invalid && !disableVerification + ? {} + : sitewide + ? {disabled: true} + : {style: {visibility: 'hidden'}, disabled: true}, + 'Verify') + ]) + ]), + h('.col-1-3', {innerHTML: '​'}), + h('.col-1-3', [ + h('.create', [ + sitewide && verified || !sitewide && !invalid + ? h('button', {type: 'submit'}, 'Create form') + : h('button', {disabled: true}, 'Create form') + ]) + ]) + ]) + ]) + } +} diff --git a/formspree/static/scss/dashboard.scss b/formspree/static/scss/dashboard.scss index 23cd6705..01dc0b84 100644 --- a/formspree/static/scss/dashboard.scss +++ b/formspree/static/scss/dashboard.scss @@ -181,3 +181,5 @@ } } } + +.red { color: $color; } diff --git a/package.json b/package.json index f033f6d3..547fcbf5 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,9 @@ }, "dependencies": { "is-valid-email": "0.0.2", - "url": "^0.11.0" + "url": "^0.11.0", + "valid-url": "^1.0.9", + "virtual-dom": "^2.1.1" }, "devDependencies": { "babel-core": "^6.6.5",