├── .babelrc ├── .eslintrc.json ├── .gitignore ├── .jshintrc ├── .npmignore ├── .travis.yml ├── LICENSE ├── dist ├── spreadsheet.js └── spreadsheet.min.js ├── example.js ├── example ├── .reactspreadsheet.gif ├── .reactspreadsheet2.gif ├── bundle.js ├── example.css └── index.html ├── gulpfile.js ├── lib ├── cell.js ├── dispatcher.js ├── helpers.js ├── row.js └── spreadsheet.js ├── package.json ├── preprocessor.js ├── readme.md ├── src ├── __tests__ │ ├── __snapshots__ │ │ ├── cell-test.js.snap │ │ └── row-test.js.snap │ ├── cell-test.js │ ├── helpers-test.js │ ├── row-test.js │ └── spreadsheet-test.js ├── cell.js ├── dispatcher.js ├── helpers.js ├── row.js └── spreadsheet.js └── styles ├── creativeworks.css └── excel.css /.babelrc: -------------------------------------------------------------------------------- 1 | // .babelrc 2 | { 3 | "presets": ["es2015", "react"] 4 | } -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es6": true, 6 | "node": true 7 | }, 8 | "parserOptions": { 9 | "ecmaFeatures": { 10 | "jsx": true 11 | }, 12 | "sourceType": "module" 13 | }, 14 | "rules": { 15 | "no-const-assign": "warn", 16 | "no-this-before-super": "warn", 17 | "no-undef": "warn", 18 | "no-unreachable": "warn", 19 | "no-unused-vars": "off", 20 | "constructor-super": "warn", 21 | "valid-typeof": "warn" 22 | } 23 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | 3 | # Logs 4 | logs 5 | *.log 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # Compiled binary addons (http://nodejs.org/api/addons.html) 22 | build/Release 23 | 24 | # Dependency directory 25 | # Commenting this out is preferred by some people, see 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 27 | node_modules 28 | 29 | # Users Environment Variables 30 | .lock-wscript 31 | 32 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "browser": true, 3 | "node": true, 4 | 5 | "curly": true, 6 | "devel": true, 7 | "globals": { 8 | }, 9 | "noempty": true, 10 | "newcap": false, 11 | "undef": true, 12 | "unused": "vars", 13 | 14 | "asi": true, 15 | "boss": true, 16 | "eqnull": true, 17 | "expr": true, 18 | "funcscope": true, 19 | "globalstrict": true, 20 | "laxbreak": true, 21 | "laxcomma": true, 22 | "loopfunc": true, 23 | "sub": true 24 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | build/ 2 | 3 | # Logs 4 | logs 5 | *.log 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # Compiled binary addons (http://nodejs.org/api/addons.html) 22 | build/Release 23 | 24 | # Dependency directory 25 | # Commenting this out is preferred by some people, see 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 27 | node_modules 28 | 29 | # Users Environment Variables 30 | .lock-wscript 31 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "5.0" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Felix Rieseberg & Microsoft Corporation 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /dist/spreadsheet.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * react-spreadsheet-component 1.0.1 (dev build at Wed, 04 Oct 2017 07:02:46 GMT) - 3 | * MIT Licensed 4 | */ 5 | !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var t;t="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,t.ReactSpreadsheet=e()}}(function(){var e;return function t(e,n,r){function i(s,a){if(!n[s]){if(!e[s]){var u="function"==typeof require&&require;if(!a&&u)return u(s,!0);if(o)return o(s,!0);var l=new Error("Cannot find module '"+s+"'");throw l.code="MODULE_NOT_FOUND",l}var c=n[s]={exports:{}};e[s][0].call(c.exports,function(t){var n=e[s][1][t];return i(n?n:t)},c,c.exports,t,e,n,r)}return n[s].exports}for(var o="function"==typeof require&&require,s=0;s0?t.cellClasses+" "+n:n,a=this.renderHeader();return a?a:(t.selected&&t.editing&&(e=l["default"].createElement("input",{className:"mousetrap",onChange:this.handleChange.bind(this),onBlur:this.handleBlur.bind(this),ref:r,defaultValue:this.props.value})),l["default"].createElement("td",{className:s,ref:t.uid.join("_")},l["default"].createElement("div",{className:"reactTableCell"},e,l["default"].createElement("span",{onDoubleClick:this.handleDoubleClick.bind(this),onClick:this.handleClick.bind(this)},o))))}},{key:"componentDidUpdate",value:function(e,t){if(this.props.editing&&this.props.selected){var n=this.refs["input_"+this.props.uid.join("_")];n.focus()}e.selected&&e.editing&&this.state.changedValue!==this.props.value&&this.props.onCellValueChange(this.props.uid,this.state.changedValue)}},{key:"handleClick",value:function(e){var t=this.refs[this.props.uid.join("_")];this.props.handleSelectCell(this.props.uid,t)}},{key:"handleHeadClick",value:function(e){var t=this.refs[this.props.uid.join("_")];f["default"].publish("headCellClicked",t,this.props.spreadsheetId)}},{key:"handleDoubleClick",value:function(e){e.preventDefault(),this.props.handleDoubleClickOnCell(this.props.uid)}},{key:"handleBlur",value:function(e){var t=this.refs["input_"+this.props.uid.join("_")].value;this.props.onCellValueChange(this.props.uid,t,e),this.props.handleCellBlur(this.props.uid),f["default"].publish("cellBlurred",this.props.uid,this.props.spreadsheetId)}},{key:"handleChange",value:function(e){var t=this.refs["input_"+this.props.uid.join("_")].value;this.setState({changedValue:t})}},{key:"renderHeader",value:function(){var e=this.props,t=e.selected?"selected":"",n=e.uid,r=e.config||{emptyValueSymbol:""},i=""!==e.value&&e.value?e.value:r.emptyValueSymbol,o=e.cellClasses&&e.cellClasses.length>0?this.props.cellClasses+" "+t:t,s=0===n[0],a=0===n[1],u=r.hasHeadRow&&0===n[0],c=r.hasHeadColumn&&0===n[1];return!(!u&&!c)&&(a&&r.hasLetterNumberHeads?i=n[0]:s&&r.hasLetterNumberHeads&&(i=d["default"].countWithLetters(n[1])),r.isHeadRowString&&s||r.isHeadColumnString&&a?l["default"].createElement("th",{className:o,ref:this.props.uid.join("_")},l["default"].createElement("div",null,l["default"].createElement("span",{onClick:this.handleHeadClick.bind(this)},i))):l["default"].createElement("th",{ref:this.props.uid.join("_")},i))}}]),t}(u.Component);t.exports=h},{"./dispatcher":2,"./helpers":3}],2:[function(e,t,n){"use strict";var r=e("mousetrap"),i=e("jquery"),o={topics:{},subscribe:function(e,t,n){this.topics[n]||(this.topics[n]=[]),this.topics[n][e]||(this.topics[n][e]=[]),this.topics[n][e].push(t)},publish:function(e,t,n){!this.topics[n]||!this.topics[n][e]||this.topics[n][e].length<1||this.topics[n][e].forEach(function(e){e(t||{})})},keyboardShortcuts:[["down","down",["keyup"]],["up","up",["keyup"]],["left","left",["keyup"]],["right","right",["keyup"]],["tab","tab",["keyup","keydown"]],["enter","enter",["keyup"]],["esc","esc",["keyup"]],["remove",["backspace","delete"],["keyup","keydown"]],["letter",["a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","x","w","y","z","1","2","3","4","5","6","7","8","9","0","=",".",",","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","X","W","Y","Z"],["keyup","keydown"]]],setupKeyboardShortcuts:function(e,t){var n=this;this.keyboardShortcuts.map(function(i){var o=i[0],s=i[1],a=i[2];a.map(function(i){r(e).bind(s,function(e){n.publish(o+"_"+i,e,t)},i)})}),window.addEventListener("keydown",function(e){[32,37,38,39,40].indexOf(e.keyCode)>-1&&"INPUT"!==i(document.activeElement)[0].tagName&&(e.preventDefault?e.preventDefault():e.returnValue=!1)},!1)}};t.exports=o},{jquery:6,mousetrap:7}],3:[function(e,t,n){"use strict";var r={firstInArray:function(e,t,n){var r=null;return e.some(function(i,o){return!!t.call(n,i,o,e)&&(r=i,!0)}),r},firstTDinArray:function(e){var t=r.firstInArray(e,function(e){return!(!e.nodeName||"TD"!==e.nodeName)});return t},equalCells:function(e,t){return!(!e||!t||e.length!==t.length)&&(e[0]===t[0]&&e[1]===t[1])},countWithLetters:function(e){var t=e%26,n=e/26|0,r=t?String.fromCharCode(64+t):(--n,"Z");return n?this.countWithLetters(n)+r:r},makeSpreadsheetId:function(){for(var e="",t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",n=0;n<5;n+=1)e+=t.charAt(Math.floor(Math.random()*t.length));return e}};t.exports=r},{}],4:[function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{"default":e}}function i(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}var a=function(){function e(e,t){for(var n=0;n0?i.click():this.extendTable(e,n)}},{key:"extendTable",value:function(e){var t,n,r=this.props.config,i=this.state.data;if("down"===e&&r.canAddRow){for(t=[],n=0;n0&&t-1 in e)}function i(e,t,n){if(se.isFunction(t))return se.grep(e,function(e,r){return!!t.call(e,r,e)!==n});if(t.nodeType)return se.grep(e,function(e){return e===t!==n});if("string"==typeof t){if(ye.test(t))return se.filter(t,e,n);t=se.filter(t,e)}return se.grep(e,function(e){return ee.call(t,e)>-1!==n})}function o(e,t){for(;(e=e[t])&&1!==e.nodeType;);return e}function s(e){var t={};return se.each(e.match(Ce)||[],function(e,n){t[n]=!0}),t}function a(){G.removeEventListener("DOMContentLoaded",a),t.removeEventListener("load",a),se.ready()}function u(){this.expando=se.expando+u.uid++}function l(e,t,n){var r;if(void 0===n&&1===e.nodeType)if(r="data-"+t.replace(De,"-$&").toLowerCase(),n=e.getAttribute(r),"string"==typeof n){try{n="true"===n||"false"!==n&&("null"===n?null:+n+""===n?+n:je.test(n)?se.parseJSON(n):n)}catch(i){}Ne.set(e,t,n)}else n=void 0;return n}function c(e,t,n,r){var i,o=1,s=20,a=r?function(){return r.cur()}:function(){return se.css(e,t,"")},u=a(),l=n&&n[3]||(se.cssNumber[t]?"":"px"),c=(se.cssNumber[t]||"px"!==l&&+u)&&qe.exec(se.css(e,t));if(c&&c[3]!==l){l=l||c[3],n=n||[],c=+u||1;do o=o||".5",c/=o,se.style(e,t,c+l);while(o!==(o=a()/u)&&1!==o&&--s)}return n&&(c=+c||+u||0,i=n[1]?c+(n[1]+1)*n[2]:+n[2],r&&(r.unit=l,r.start=c,r.end=i)),i}function f(e,t){var n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[];return void 0===t||t&&se.nodeName(e,t)?se.merge([e],n):n}function p(e,t){for(var n=0,r=e.length;n-1)i&&i.push(o);else if(l=se.contains(o.ownerDocument,o),s=f(d.appendChild(o),"script"),l&&p(s),n)for(c=0;o=s[c++];)Pe.test(o.type||"")&&n.push(o);return d}function h(){return!0}function g(){return!1}function y(){try{return G.activeElement}catch(e){}}function v(e,t,n,r,i,o){var s,a;if("object"==typeof t){"string"!=typeof n&&(r=r||n,n=void 0);for(a in t)v(e,a,n,r,t[a],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),i===!1)i=g;else if(!i)return e;return 1===o&&(s=i,i=function(e){return se().off(e),s.apply(this,arguments)},i.guid=s.guid||(s.guid=se.guid++)),e.each(function(){se.event.add(this,t,i,r,n)})}function m(e,t){return se.nodeName(e,"table")&&se.nodeName(11!==t.nodeType?t:t.firstChild,"tr")?e.getElementsByTagName("tbody")[0]||e.appendChild(e.ownerDocument.createElement("tbody")):e}function b(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function x(e){var t=Xe.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function w(e,t){var n,r,i,o,s,a,u,l;if(1===t.nodeType){if(Se.hasData(e)&&(o=Se.access(e),s=Se.set(t,o),l=o.events)){delete s.handle,s.events={};for(i in l)for(n=0,r=l[i].length;n1&&"string"==typeof g&&!ie.checkClone&&Ve.test(g))return e.each(function(i){var o=e.eq(i);y&&(t[0]=g.call(this,i,o.html())),k(o,t,n,r)});if(p&&(i=d(t,e[0].ownerDocument,!1,e,r),o=i.firstChild,1===i.childNodes.length&&(i=o),o||r)){for(s=se.map(f(i,"script"),b),a=s.length;c")).appendTo(t.documentElement),t=Ue[0].contentDocument,t.write(),t.close(),n=E(e,t),Ue.detach()),Ke[e]=n),n}function N(e,t,n){var r,i,o,s,a=e.style;return n=n||Qe(e),s=n?n.getPropertyValue(t)||n[t]:void 0,""!==s&&void 0!==s||se.contains(e.ownerDocument,e)||(s=se.style(e,t)),n&&!ie.pixelMarginRight()&&Ge.test(s)&&Ye.test(t)&&(r=a.width,i=a.minWidth,o=a.maxWidth,a.minWidth=a.maxWidth=a.width=s,s=n.width,a.width=r,a.minWidth=i,a.maxWidth=o),void 0!==s?s+"":s}function j(e,t){return{get:function(){return e()?void delete this.get:(this.get=t).apply(this,arguments)}}}function D(e){if(e in it)return e;for(var t=e[0].toUpperCase()+e.slice(1),n=rt.length;n--;)if(e=rt[n]+t,e in it)return e}function A(e,t,n){var r=qe.exec(t);return r?Math.max(0,r[2]-(n||0))+(r[3]||"px"):t}function q(e,t,n,r,i){for(var o=n===(r?"border":"content")?4:"width"===t?1:0,s=0;o<4;o+=2)"margin"===n&&(s+=se.css(e,n+Le[o],!0,i)),r?("content"===n&&(s-=se.css(e,"padding"+Le[o],!0,i)),"margin"!==n&&(s-=se.css(e,"border"+Le[o]+"Width",!0,i))):(s+=se.css(e,"padding"+Le[o],!0,i),"padding"!==n&&(s+=se.css(e,"border"+Le[o]+"Width",!0,i)));return s}function L(e,t,n){var r=!0,i="width"===t?e.offsetWidth:e.offsetHeight,o=Qe(e),s="border-box"===se.css(e,"boxSizing",!1,o);if(i<=0||null==i){if(i=N(e,t,o),(i<0||null==i)&&(i=e.style[t]),Ge.test(i))return i;r=s&&(ie.boxSizingReliable()||i===e.style[t]),i=parseFloat(i)||0}return i+q(e,t,n||(s?"border":"content"),r,o)+"px"}function O(e,t){for(var n,r,i,o=[],s=0,a=e.length;s=0&&n=0},isPlainObject:function(e){var t;if("object"!==se.type(e)||e.nodeType||se.isWindow(e))return!1;if(e.constructor&&!re.call(e,"constructor")&&!re.call(e.constructor.prototype||{},"isPrototypeOf"))return!1;for(t in e);return void 0===t||re.call(e,t)},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},type:function(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?te[ne.call(e)]||"object":typeof e},globalEval:function(e){var t,n=eval;e=se.trim(e),e&&(1===e.indexOf("use strict")?(t=G.createElement("script"),t.text=e,G.head.appendChild(t).parentNode.removeChild(t)):n(e))},camelCase:function(e){return e.replace(ue,"ms-").replace(le,ce)},nodeName:function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()},each:function(e,t){var n,i=0;if(r(e))for(n=e.length;iC.cacheLength&&delete e[t.shift()],e[n+" "]=r}var t=[];return e}function r(e){return e[F]=!0,e}function i(e){var t=L.createElement("div");try{return!!e(t)}catch(n){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function o(e,t){for(var n=e.split("|"),r=n.length;r--;)C.attrHandle[n[r]]=t}function s(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&(~t.sourceIndex||U)-(~e.sourceIndex||U);if(r)return r;if(n)for(;n=n.nextSibling;)if(n===t)return-1; 6 | return e?1:-1}function a(e){return function(t){var n=t.nodeName.toLowerCase();return"input"===n&&t.type===e}}function u(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function l(e){return r(function(t){return t=+t,r(function(n,r){for(var i,o=e([],n.length,t),s=o.length;s--;)n[i=o[s]]&&(n[i]=!(r[i]=n[i]))})})}function c(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}function f(){}function p(e){for(var t=0,n=e.length,r="";t1?function(t,n,r){for(var i=e.length;i--;)if(!e[i](t,n,r))return!1;return!0}:e[0]}function g(e,n,r){for(var i=0,o=n.length;i-1&&(r[l]=!(s[l]=f))}}else b=y(b===s?b.splice(h,b.length):b),o?o(null,s,b,u):J.apply(s,b)})}function m(e){for(var t,n,r,i=e.length,o=C.relative[e[0].type],s=o||C.relative[" "],a=o?1:0,u=d(function(e){return e===t},s,!0),l=d(function(e){return ee(t,e)>-1},s,!0),c=[function(e,n,r){var i=!o&&(r||n!==j)||((t=n).nodeType?u(e,n,r):l(e,n,r));return t=null,i}];a1&&h(c),a>1&&p(e.slice(0,a-1).concat({value:" "===e[a-2].type?"*":""})).replace(ae,"$1"),n,a0,o=e.length>0,s=function(r,s,a,u,l){var c,f,p,d=0,h="0",g=r&&[],v=[],m=j,b=r||o&&C.find.TAG("*",l),x=B+=null==m?1:Math.random()||.1,w=b.length;for(l&&(j=s===L||s||l);h!==w&&null!=(c=b[h]);h++){if(o&&c){for(f=0,s||c.ownerDocument===L||(q(c),a=!_);p=e[f++];)if(p(c,s||L,a)){u.push(c);break}l&&(B=x)}i&&((c=!p&&c)&&d--,r&&g.push(c))}if(d+=h,i&&h!==d){for(f=0;p=n[f++];)p(g,v,s,a);if(r){if(d>0)for(;h--;)g[h]||v[h]||(v[h]=G.call(u));v=y(v)}J.apply(u,v),l&&!r&&v.length>0&&d+n.length>1&&t.uniqueSort(u)}return l&&(B=x,j=m),g};return i?r(s):s}var x,w,C,k,T,E,S,N,j,D,A,q,L,O,_,H,P,R,M,F="sizzle"+1*new Date,I=e.document,B=0,W=0,$=n(),V=n(),X=n(),z=function(e,t){return e===t&&(A=!0),0},U=1<<31,K={}.hasOwnProperty,Y=[],G=Y.pop,Q=Y.push,J=Y.push,Z=Y.slice,ee=function(e,t){for(var n=0,r=e.length;n+~]|"+ne+")"+ne+"*"),ce=new RegExp("="+ne+"*([^\\]'\"]*?)"+ne+"*\\]","g"),fe=new RegExp(oe),pe=new RegExp("^"+re+"$"),de={ID:new RegExp("^#("+re+")"),CLASS:new RegExp("^\\.("+re+")"),TAG:new RegExp("^("+re+"|[*])"),ATTR:new RegExp("^"+ie),PSEUDO:new RegExp("^"+oe),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+ne+"*(even|odd|(([+-]|)(\\d*)n|)"+ne+"*(?:([+-]|)"+ne+"*(\\d+)|))"+ne+"*\\)|)","i"),bool:new RegExp("^(?:"+te+")$","i"),needsContext:new RegExp("^"+ne+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+ne+"*((?:-\\d)?\\d*)"+ne+"*\\)|)(?=[^-]|$)","i")},he=/^(?:input|select|textarea|button)$/i,ge=/^h\d$/i,ye=/^[^{]+\{\s*\[native \w/,ve=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,me=/[+~]/,be=/'|\\/g,xe=new RegExp("\\\\([\\da-f]{1,6}"+ne+"?|("+ne+")|.)","ig"),we=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)},Ce=function(){q()};try{J.apply(Y=Z.call(I.childNodes),I.childNodes),Y[I.childNodes.length].nodeType}catch(ke){J={apply:Y.length?function(e,t){Q.apply(e,Z.call(t))}:function(e,t){for(var n=e.length,r=0;e[n++]=t[r++];);e.length=n-1}}}w=t.support={},T=t.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return!!t&&"HTML"!==t.nodeName},q=t.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:I;return r!==L&&9===r.nodeType&&r.documentElement?(L=r,O=L.documentElement,_=!T(L),(n=L.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",Ce,!1):n.attachEvent&&n.attachEvent("onunload",Ce)),w.attributes=i(function(e){return e.className="i",!e.getAttribute("className")}),w.getElementsByTagName=i(function(e){return e.appendChild(L.createComment("")),!e.getElementsByTagName("*").length}),w.getElementsByClassName=ye.test(L.getElementsByClassName),w.getById=i(function(e){return O.appendChild(e).id=F,!L.getElementsByName||!L.getElementsByName(F).length}),w.getById?(C.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&_){var n=t.getElementById(e);return n?[n]:[]}},C.filter.ID=function(e){var t=e.replace(xe,we);return function(e){return e.getAttribute("id")===t}}):(delete C.find.ID,C.filter.ID=function(e){var t=e.replace(xe,we);return function(e){var n="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return n&&n.value===t}}),C.find.TAG=w.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):w.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){for(;n=o[i++];)1===n.nodeType&&r.push(n);return r}return o},C.find.CLASS=w.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&_)return t.getElementsByClassName(e)},P=[],H=[],(w.qsa=ye.test(L.querySelectorAll))&&(i(function(e){O.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&H.push("[*^$]="+ne+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||H.push("\\["+ne+"*(?:value|"+te+")"),e.querySelectorAll("[id~="+F+"-]").length||H.push("~="),e.querySelectorAll(":checked").length||H.push(":checked"),e.querySelectorAll("a#"+F+"+*").length||H.push(".#.+[+~]")}),i(function(e){var t=L.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&H.push("name"+ne+"*[*^$|!~]?="),e.querySelectorAll(":enabled").length||H.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),H.push(",.*:")})),(w.matchesSelector=ye.test(R=O.matches||O.webkitMatchesSelector||O.mozMatchesSelector||O.oMatchesSelector||O.msMatchesSelector))&&i(function(e){w.disconnectedMatch=R.call(e,"div"),R.call(e,"[s!='']:x"),P.push("!=",oe)}),H=H.length&&new RegExp(H.join("|")),P=P.length&&new RegExp(P.join("|")),t=ye.test(O.compareDocumentPosition),M=t||ye.test(O.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)for(;t=t.parentNode;)if(t===e)return!0;return!1},z=t?function(e,t){if(e===t)return A=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n?n:(n=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1,1&n||!w.sortDetached&&t.compareDocumentPosition(e)===n?e===L||e.ownerDocument===I&&M(I,e)?-1:t===L||t.ownerDocument===I&&M(I,t)?1:D?ee(D,e)-ee(D,t):0:4&n?-1:1)}:function(e,t){if(e===t)return A=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],u=[t];if(!i||!o)return e===L?-1:t===L?1:i?-1:o?1:D?ee(D,e)-ee(D,t):0;if(i===o)return s(e,t);for(n=e;n=n.parentNode;)a.unshift(n);for(n=t;n=n.parentNode;)u.unshift(n);for(;a[r]===u[r];)r++;return r?s(a[r],u[r]):a[r]===I?-1:u[r]===I?1:0},L):L},t.matches=function(e,n){return t(e,null,null,n)},t.matchesSelector=function(e,n){if((e.ownerDocument||e)!==L&&q(e),n=n.replace(ce,"='$1']"),w.matchesSelector&&_&&!X[n+" "]&&(!P||!P.test(n))&&(!H||!H.test(n)))try{var r=R.call(e,n);if(r||w.disconnectedMatch||e.document&&11!==e.document.nodeType)return r}catch(i){}return t(n,L,null,[e]).length>0},t.contains=function(e,t){return(e.ownerDocument||e)!==L&&q(e),M(e,t)},t.attr=function(e,t){(e.ownerDocument||e)!==L&&q(e);var n=C.attrHandle[t.toLowerCase()],r=n&&K.call(C.attrHandle,t.toLowerCase())?n(e,t,!_):void 0;return void 0!==r?r:w.attributes||!_?e.getAttribute(t):(r=e.getAttributeNode(t))&&r.specified?r.value:null},t.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},t.uniqueSort=function(e){var t,n=[],r=0,i=0;if(A=!w.detectDuplicates,D=!w.sortStable&&e.slice(0),e.sort(z),A){for(;t=e[i++];)t===e[i]&&(r=n.push(i));for(;r--;)e.splice(n[r],1)}return D=null,e},k=t.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(1===i||9===i||11===i){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=k(e)}else if(3===i||4===i)return e.nodeValue}else for(;t=e[r++];)n+=k(t);return n},C=t.selectors={cacheLength:50,createPseudo:r,match:de,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(xe,we),e[3]=(e[3]||e[4]||e[5]||"").replace(xe,we),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||t.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&t.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return de.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&fe.test(n)&&(t=E(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(xe,we).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=$[e+" "];return t||(t=new RegExp("(^|"+ne+")"+e+"("+ne+"|$)"))&&$(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(e,n,r){return function(i){var o=t.attr(i,e);return null==o?"!="===n:!n||(o+="","="===n?o===r:"!="===n?o!==r:"^="===n?r&&0===o.indexOf(r):"*="===n?r&&o.indexOf(r)>-1:"$="===n?r&&o.slice(-r.length)===r:"~="===n?(" "+o.replace(se," ")+" ").indexOf(r)>-1:"|="===n&&(o===r||o.slice(0,r.length+1)===r+"-"))}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),s="last"!==e.slice(-4),a="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,u){var l,c,f,p,d,h,g=o!==s?"nextSibling":"previousSibling",y=t.parentNode,v=a&&t.nodeName.toLowerCase(),m=!u&&!a,b=!1;if(y){if(o){for(;g;){for(p=t;p=p[g];)if(a?p.nodeName.toLowerCase()===v:1===p.nodeType)return!1;h=g="only"===e&&!h&&"nextSibling"}return!0}if(h=[s?y.firstChild:y.lastChild],s&&m){for(p=y,f=p[F]||(p[F]={}),c=f[p.uniqueID]||(f[p.uniqueID]={}),l=c[e]||[],d=l[0]===B&&l[1],b=d&&l[2],p=d&&y.childNodes[d];p=++d&&p&&p[g]||(b=d=0)||h.pop();)if(1===p.nodeType&&++b&&p===t){c[e]=[B,d,b];break}}else if(m&&(p=t,f=p[F]||(p[F]={}),c=f[p.uniqueID]||(f[p.uniqueID]={}),l=c[e]||[],d=l[0]===B&&l[1],b=d),b===!1)for(;(p=++d&&p&&p[g]||(b=d=0)||h.pop())&&((a?p.nodeName.toLowerCase()!==v:1!==p.nodeType)||!++b||(m&&(f=p[F]||(p[F]={}),c=f[p.uniqueID]||(f[p.uniqueID]={}),c[e]=[B,b]),p!==t)););return b-=i,b===r||b%r===0&&b/r>=0}}},PSEUDO:function(e,n){var i,o=C.pseudos[e]||C.setFilters[e.toLowerCase()]||t.error("unsupported pseudo: "+e);return o[F]?o(n):o.length>1?(i=[e,e,"",n],C.setFilters.hasOwnProperty(e.toLowerCase())?r(function(e,t){for(var r,i=o(e,n),s=i.length;s--;)r=ee(e,i[s]),e[r]=!(t[r]=i[s])}):function(e){return o(e,0,i)}):o}},pseudos:{not:r(function(e){var t=[],n=[],i=S(e.replace(ae,"$1"));return i[F]?r(function(e,t,n,r){for(var o,s=i(e,null,r,[]),a=e.length;a--;)(o=s[a])&&(e[a]=!(t[a]=o))}):function(e,r,o){return t[0]=e,i(t,null,o,n),t[0]=null,!n.pop()}}),has:r(function(e){return function(n){return t(e,n).length>0}}),contains:r(function(e){return e=e.replace(xe,we),function(t){return(t.textContent||t.innerText||k(t)).indexOf(e)>-1}}),lang:r(function(e){return pe.test(e||"")||t.error("unsupported lang: "+e),e=e.replace(xe,we).toLowerCase(),function(t){var n;do if(n=_?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return n=n.toLowerCase(),n===e||0===n.indexOf(e+"-");while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===O},focus:function(e){return e===L.activeElement&&(!L.hasFocus||L.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:function(e){return e.disabled===!1},disabled:function(e){return e.disabled===!0},checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,e.selected===!0},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!C.pseudos.empty(e)},header:function(e){return ge.test(e.nodeName)},input:function(e){return he.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:l(function(){return[0]}),last:l(function(e,t){return[t-1]}),eq:l(function(e,t,n){return[n<0?n+t:n]}),even:l(function(e,t){for(var n=0;n=0;)e.push(r);return e}),gt:l(function(e,t,n){for(var r=n<0?n+t:n;++r2&&"ID"===(s=o[0]).type&&w.getById&&9===t.nodeType&&_&&C.relative[o[1].type]){if(t=(C.find.ID(s.matches[0].replace(xe,we),t)||[])[0],!t)return n;l&&(t=t.parentNode),e=e.slice(o.shift().value.length)}for(i=de.needsContext.test(e)?0:o.length;i--&&(s=o[i],!C.relative[a=s.type]);)if((u=C.find[a])&&(r=u(s.matches[0].replace(xe,we),me.test(o[0].type)&&c(t.parentNode)||t))){if(o.splice(i,1),e=r.length&&p(o),!e)return J.apply(n,r),n;break}}return(l||S(e,f))(r,t,!_,n,!t||me.test(e)&&c(t.parentNode)||t),n},w.sortStable=F.split("").sort(z).join("")===F,w.detectDuplicates=!!A,q(),w.sortDetached=i(function(e){return 1&e.compareDocumentPosition(L.createElement("div"))}),i(function(e){return e.innerHTML="","#"===e.firstChild.getAttribute("href")})||o("type|href|height|width",function(e,t,n){if(!n)return e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),w.attributes&&i(function(e){return e.innerHTML="",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||o("value",function(e,t,n){if(!n&&"input"===e.nodeName.toLowerCase())return e.defaultValue}),i(function(e){return null==e.getAttribute("disabled")})||o(te,function(e,t,n){var r;if(!n)return e[t]===!0?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null}),t}(t);se.find=fe,se.expr=fe.selectors,se.expr[":"]=se.expr.pseudos,se.uniqueSort=se.unique=fe.uniqueSort,se.text=fe.getText,se.isXMLDoc=fe.isXML,se.contains=fe.contains;var pe=function(e,t,n){for(var r=[],i=void 0!==n;(e=e[t])&&9!==e.nodeType;)if(1===e.nodeType){if(i&&se(e).is(n))break;r.push(e)}return r},de=function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n},he=se.expr.match.needsContext,ge=/^<([\w-]+)\s*\/?>(?:<\/\1>|)$/,ye=/^.[^:#\[\.,]*$/;se.filter=function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?se.find.matchesSelector(r,e)?[r]:[]:se.find.matches(e,se.grep(t,function(e){return 1===e.nodeType}))},se.fn.extend({find:function(e){var t,n=this.length,r=[],i=this;if("string"!=typeof e)return this.pushStack(se(e).filter(function(){for(t=0;t1?se.unique(r):r),r.selector=this.selector?this.selector+" "+e:e,r},filter:function(e){return this.pushStack(i(this,e||[],!1))},not:function(e){return this.pushStack(i(this,e||[],!0))},is:function(e){return!!i(this,"string"==typeof e&&he.test(e)?se(e):e||[],!1).length}});var ve,me=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,be=se.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||ve,"string"==typeof e){if(r="<"===e[0]&&">"===e[e.length-1]&&e.length>=3?[null,e,null]:me.exec(e),!r||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof se?t[0]:t,se.merge(this,se.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:G,!0)),ge.test(r[1])&&se.isPlainObject(t))for(r in t)se.isFunction(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return i=G.getElementById(r[2]),i&&i.parentNode&&(this.length=1,this[0]=i),this.context=G,this.selector=e,this}return e.nodeType?(this.context=this[0]=e,this.length=1,this):se.isFunction(e)?void 0!==n.ready?n.ready(e):e(se):(void 0!==e.selector&&(this.selector=e.selector,this.context=e.context),se.makeArray(e,this))};be.prototype=se.fn,ve=se(G);var xe=/^(?:parents|prev(?:Until|All))/,we={children:!0,contents:!0,next:!0,prev:!0};se.fn.extend({has:function(e){var t=se(e,this),n=t.length;return this.filter(function(){for(var e=0;e-1:1===n.nodeType&&se.find.matchesSelector(n,e))){o.push(n);break}return this.pushStack(o.length>1?se.uniqueSort(o):o)},index:function(e){return e?"string"==typeof e?ee.call(se(e),this[0]):ee.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(se.uniqueSort(se.merge(this.get(),se(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}}),se.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return pe(e,"parentNode")},parentsUntil:function(e,t,n){return pe(e,"parentNode",n)},next:function(e){return o(e,"nextSibling")},prev:function(e){return o(e,"previousSibling")},nextAll:function(e){return pe(e,"nextSibling")},prevAll:function(e){return pe(e,"previousSibling")},nextUntil:function(e,t,n){return pe(e,"nextSibling",n)},prevUntil:function(e,t,n){return pe(e,"previousSibling",n)},siblings:function(e){return de((e.parentNode||{}).firstChild,e)},children:function(e){return de(e.firstChild)},contents:function(e){return e.contentDocument||se.merge([],e.childNodes)}},function(e,t){se.fn[e]=function(n,r){var i=se.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=se.filter(r,i)),this.length>1&&(we[e]||se.uniqueSort(i),xe.test(e)&&i.reverse()),this.pushStack(i)}});var Ce=/\S+/g;se.Callbacks=function(e){e="string"==typeof e?s(e):se.extend({},e);var t,n,r,i,o=[],a=[],u=-1,l=function(){for(i=e.once,r=t=!0;a.length;u=-1)for(n=a.shift();++u-1;)o.splice(n,1),n<=u&&u--}),this},has:function(e){return e?se.inArray(e,o)>-1:o.length>0},empty:function(){return o&&(o=[]),this},disable:function(){return i=a=[],o=n="",this},disabled:function(){return!o},lock:function(){return i=a=[],n||(o=n=""),this},locked:function(){return!!i},fireWith:function(e,n){return i||(n=n||[],n=[e,n.slice?n.slice():n],a.push(n),t||l()),this},fire:function(){return c.fireWith(this,arguments),this},fired:function(){return!!r}};return c},se.extend({Deferred:function(e){var t=[["resolve","done",se.Callbacks("once memory"),"resolved"],["reject","fail",se.Callbacks("once memory"),"rejected"],["notify","progress",se.Callbacks("memory")]],n="pending",r={state:function(){return n},always:function(){return i.done(arguments).fail(arguments),this},then:function(){var e=arguments;return se.Deferred(function(n){se.each(t,function(t,o){var s=se.isFunction(e[t])&&e[t];i[o[1]](function(){var e=s&&s.apply(this,arguments);e&&se.isFunction(e.promise)?e.promise().progress(n.notify).done(n.resolve).fail(n.reject):n[o[0]+"With"](this===r?n.promise():this,s?[e]:arguments)})}),e=null}).promise()},promise:function(e){return null!=e?se.extend(e,r):r}},i={};return r.pipe=r.then,se.each(t,function(e,o){var s=o[2],a=o[3];r[o[1]]=s.add,a&&s.add(function(){n=a},t[1^e][2].disable,t[2][2].lock),i[o[0]]=function(){return i[o[0]+"With"](this===i?r:this,arguments),this},i[o[0]+"With"]=s.fireWith}),r.promise(i),e&&e.call(i,i),i},when:function(e){var t,n,r,i=0,o=Q.call(arguments),s=o.length,a=1!==s||e&&se.isFunction(e.promise)?s:0,u=1===a?e:se.Deferred(),l=function(e,n,r){return function(i){n[e]=this,r[e]=arguments.length>1?Q.call(arguments):i,r===t?u.notifyWith(n,r):--a||u.resolveWith(n,r)}};if(s>1)for(t=new Array(s),n=new Array(s),r=new Array(s);i0||(ke.resolveWith(G,[se]),se.fn.triggerHandler&&(se(G).triggerHandler("ready"),se(G).off("ready"))))}}),se.ready.promise=function(e){return ke||(ke=se.Deferred(),"complete"===G.readyState||"loading"!==G.readyState&&!G.documentElement.doScroll?t.setTimeout(se.ready):(G.addEventListener("DOMContentLoaded",a),t.addEventListener("load",a))),ke.promise(e)},se.ready.promise();var Te=function(e,t,n,r,i,o,s){var a=0,u=e.length,l=null==n;if("object"===se.type(n)){i=!0;for(a in n)Te(e,t,a,n[a],!0,o,s)}else if(void 0!==r&&(i=!0,se.isFunction(r)||(s=!0),l&&(s?(t.call(e,r),t=null):(l=t,t=function(e,t,n){return l.call(se(e),n)})),t))for(;a-1&&void 0!==n&&Ne.set(this,e,t)})},null,t,arguments.length>1,null,!0)},removeData:function(e){return this.each(function(){Ne.remove(this,e)})}}),se.extend({queue:function(e,t,n){var r;if(e)return t=(t||"fx")+"queue",r=Se.get(e,t),n&&(!r||se.isArray(n)?r=Se.access(e,t,se.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||"fx";var n=se.queue(e,t),r=n.length,i=n.shift(),o=se._queueHooks(e,t),s=function(){se.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,s,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return Se.get(e,n)||Se.access(e,n,{empty:se.Callbacks("once memory").add(function(){Se.remove(e,[t+"queue",n])})})}}),se.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),arguments.length",""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};Re.optgroup=Re.option,Re.tbody=Re.tfoot=Re.colgroup=Re.caption=Re.thead,Re.th=Re.td;var Me=/<|&#?\w+;/;!function(){var e=G.createDocumentFragment(),t=e.appendChild(G.createElement("div")),n=G.createElement("input");n.setAttribute("type","radio"),n.setAttribute("checked","checked"),n.setAttribute("name","t"),t.appendChild(n),ie.checkClone=t.cloneNode(!0).cloneNode(!0).lastChild.checked,t.innerHTML="",ie.noCloneChecked=!!t.cloneNode(!0).lastChild.defaultValue}();var Fe=/^key/,Ie=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Be=/^([^.]*)(?:\.(.+)|)/;se.event={global:{},add:function(e,t,n,r,i){var o,s,a,u,l,c,f,p,d,h,g,y=Se.get(e);if(y)for(n.handler&&(o=n,n=o.handler,i=o.selector),n.guid||(n.guid=se.guid++),(u=y.events)||(u=y.events={}),(s=y.handle)||(s=y.handle=function(t){return"undefined"!=typeof se&&se.event.triggered!==t.type?se.event.dispatch.apply(e,arguments):void 0}),t=(t||"").match(Ce)||[""],l=t.length;l--;)a=Be.exec(t[l])||[],d=g=a[1],h=(a[2]||"").split(".").sort(),d&&(f=se.event.special[d]||{},d=(i?f.delegateType:f.bindType)||d,f=se.event.special[d]||{},c=se.extend({type:d,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&se.expr.match.needsContext.test(i),namespace:h.join(".")},o),(p=u[d])||(p=u[d]=[],p.delegateCount=0,f.setup&&f.setup.call(e,r,h,s)!==!1||e.addEventListener&&e.addEventListener(d,s)),f.add&&(f.add.call(e,c),c.handler.guid||(c.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,c):p.push(c),se.event.global[d]=!0)},remove:function(e,t,n,r,i){var o,s,a,u,l,c,f,p,d,h,g,y=Se.hasData(e)&&Se.get(e);if(y&&(u=y.events)){for(t=(t||"").match(Ce)||[""],l=t.length;l--;)if(a=Be.exec(t[l])||[],d=g=a[1],h=(a[2]||"").split(".").sort(),d){for(f=se.event.special[d]||{},d=(r?f.delegateType:f.bindType)||d,p=u[d]||[],a=a[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),s=o=p.length;o--;)c=p[o],!i&&g!==c.origType||n&&n.guid!==c.guid||a&&!a.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(p.splice(o,1),c.selector&&p.delegateCount--,f.remove&&f.remove.call(e,c));s&&!p.length&&(f.teardown&&f.teardown.call(e,h,y.handle)!==!1||se.removeEvent(e,d,y.handle),delete u[d])}else for(d in u)se.event.remove(e,d+t[l],n,r,!0);se.isEmptyObject(u)&&Se.remove(e,"handle events")}},dispatch:function(e){e=se.event.fix(e);var t,n,r,i,o,s=[],a=Q.call(arguments),u=(Se.get(this,"events")||{})[e.type]||[],l=se.event.special[e.type]||{};if(a[0]=e,e.delegateTarget=this,!l.preDispatch||l.preDispatch.call(this,e)!==!1){for(s=se.event.handlers.call(this,e,u),t=0;(i=s[t++])&&!e.isPropagationStopped();)for(e.currentTarget=i.elem,n=0;(o=i.handlers[n++])&&!e.isImmediatePropagationStopped();)e.rnamespace&&!e.rnamespace.test(o.namespace)||(e.handleObj=o,e.data=o.data,r=((se.event.special[o.origType]||{}).handle||o.handler).apply(i.elem,a),void 0!==r&&(e.result=r)===!1&&(e.preventDefault(),e.stopPropagation()));return l.postDispatch&&l.postDispatch.call(this,e),e.result}},handlers:function(e,t){var n,r,i,o,s=[],a=t.delegateCount,u=e.target;if(a&&u.nodeType&&("click"!==e.type||isNaN(e.button)||e.button<1))for(;u!==this;u=u.parentNode||this)if(1===u.nodeType&&(u.disabled!==!0||"click"!==e.type)){for(r=[],n=0;n-1:se.find(i,this,null,[u]).length),r[i]&&r.push(o);r.length&&s.push({elem:u,handlers:r})}return a]*)\/>/gi,$e=/\s*$/g;se.extend({htmlPrefilter:function(e){return e.replace(We,"<$1>")},clone:function(e,t,n){var r,i,o,s,a=e.cloneNode(!0),u=se.contains(e.ownerDocument,e);if(!(ie.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||se.isXMLDoc(e)))for(s=f(a),o=f(e),r=0,i=o.length;r0&&p(s,!u&&f(e,"script")),a},cleanData:function(e){for(var t,n,r,i=se.event.special,o=0;void 0!==(n=e[o]);o++)if(Ee(n)){if(t=n[Se.expando]){if(t.events)for(r in t.events)i[r]?se.event.remove(n,r):se.removeEvent(n,r,t.handle);n[Se.expando]=void 0}n[Ne.expando]&&(n[Ne.expando]=void 0)}}}),se.fn.extend({domManip:k,detach:function(e){return T(this,e,!0)},remove:function(e){return T(this,e)},text:function(e){return Te(this,function(e){return void 0===e?se.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=e)})},null,e,arguments.length)},append:function(){return k(this,arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=m(this,e);t.appendChild(e)}})},prepend:function(){return k(this,arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=m(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return k(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return k(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},empty:function(){for(var e,t=0;null!=(e=this[t]);t++)1===e.nodeType&&(se.cleanData(f(e,!1)),e.textContent="");return this},clone:function(e,t){return e=null!=e&&e,t=null==t?e:t,this.map(function(){return se.clone(this,e,t)})},html:function(e){return Te(this,function(e){var t=this[0]||{},n=0,r=this.length;if(void 0===e&&1===t.nodeType)return t.innerHTML;if("string"==typeof e&&!$e.test(e)&&!Re[(He.exec(e)||["",""])[1].toLowerCase()]){e=se.htmlPrefilter(e);try{for(;n1)},show:function(){return O(this,!0)},hide:function(){return O(this)},toggle:function(e){return"boolean"==typeof e?e?this.show():this.hide():this.each(function(){Oe(this)?se(this).show():se(this).hide()})}}),se.Tween=_,_.prototype={constructor:_,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||se.easing._default,this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(se.cssNumber[n]?"":"px")},cur:function(){var e=_.propHooks[this.prop];return e&&e.get?e.get(this):_.propHooks._default.get(this)},run:function(e){var t,n=_.propHooks[this.prop];return this.options.duration?this.pos=t=se.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):this.pos=t=e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):_.propHooks._default.set(this),this}},_.prototype.init.prototype=_.prototype,_.propHooks={_default:{get:function(e){var t;return 1!==e.elem.nodeType||null!=e.elem[e.prop]&&null==e.elem.style[e.prop]?e.elem[e.prop]:(t=se.css(e.elem,e.prop,""),t&&"auto"!==t?t:0)},set:function(e){se.fx.step[e.prop]?se.fx.step[e.prop](e):1!==e.elem.nodeType||null==e.elem.style[se.cssProps[e.prop]]&&!se.cssHooks[e.prop]?e.elem[e.prop]=e.now:se.style(e.elem,e.prop,e.now+e.unit)}}},_.propHooks.scrollTop=_.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},se.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2},_default:"swing"},se.fx=_.prototype.init,se.fx.step={};var ot,st,at=/^(?:toggle|show|hide)$/,ut=/queueHooks$/;se.Animation=se.extend(I,{tweeners:{"*":[function(e,t){var n=this.createTween(e,t);return c(n.elem,e,qe.exec(t),n),n}]},tweener:function(e,t){se.isFunction(e)?(t=e,e=["*"]):e=e.match(Ce);for(var n,r=0,i=e.length;r1)},removeAttr:function(e){return this.each(function(){se.removeAttr(this,e)})}}),se.extend({attr:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return"undefined"==typeof e.getAttribute?se.prop(e,t,n):(1===o&&se.isXMLDoc(e)||(t=t.toLowerCase(),i=se.attrHooks[t]||(se.expr.match.bool.test(t)?lt:void 0)),void 0!==n?null===n?void se.removeAttr(e,t):i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:(e.setAttribute(t,n+""),n):i&&"get"in i&&null!==(r=i.get(e,t))?r:(r=se.find.attr(e,t),null==r?void 0:r))},attrHooks:{type:{set:function(e,t){if(!ie.radioValue&&"radio"===t&&se.nodeName(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},removeAttr:function(e,t){var n,r,i=0,o=t&&t.match(Ce);if(o&&1===e.nodeType)for(;n=o[i++];)r=se.propFix[n]||n,se.expr.match.bool.test(n)&&(e[r]=!1),e.removeAttribute(n)}}),lt={set:function(e,t,n){return t===!1?se.removeAttr(e,n):e.setAttribute(n,n),n}},se.each(se.expr.match.bool.source.match(/\w+/g),function(e,t){var n=ct[t]||se.find.attr;ct[t]=function(e,t,r){var i,o;return r||(o=ct[t],ct[t]=i,i=null!=n(e,t,r)?t.toLowerCase():null,ct[t]=o),i}});var ft=/^(?:input|select|textarea|button)$/i,pt=/^(?:a|area)$/i;se.fn.extend({prop:function(e,t){return Te(this,se.prop,e,t,arguments.length>1)},removeProp:function(e){return this.each(function(){delete this[se.propFix[e]||e]})}}),se.extend({prop:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return 1===o&&se.isXMLDoc(e)||(t=se.propFix[t]||t,i=se.propHooks[t]),void 0!==n?i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:e[t]=n:i&&"get"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){var t=se.find.attr(e,"tabindex");return t?parseInt(t,10):ft.test(e.nodeName)||pt.test(e.nodeName)&&e.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),ie.optSelected||(se.propHooks.selected={get:function(e){var t=e.parentNode;return t&&t.parentNode&&t.parentNode.selectedIndex,null},set:function(e){var t=e.parentNode;t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex)}}),se.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){se.propFix[this.toLowerCase()]=this});var dt=/[\t\r\n\f]/g;se.fn.extend({addClass:function(e){var t,n,r,i,o,s,a,u=0;if(se.isFunction(e))return this.each(function(t){se(this).addClass(e.call(this,t,B(this)))});if("string"==typeof e&&e)for(t=e.match(Ce)||[];n=this[u++];)if(i=B(n),r=1===n.nodeType&&(" "+i+" ").replace(dt," ")){for(s=0;o=t[s++];)r.indexOf(" "+o+" ")<0&&(r+=o+" ");a=se.trim(r),i!==a&&n.setAttribute("class",a)}return this},removeClass:function(e){var t,n,r,i,o,s,a,u=0;if(se.isFunction(e))return this.each(function(t){se(this).removeClass(e.call(this,t,B(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof e&&e)for(t=e.match(Ce)||[];n=this[u++];)if(i=B(n),r=1===n.nodeType&&(" "+i+" ").replace(dt," ")){for(s=0;o=t[s++];)for(;r.indexOf(" "+o+" ")>-1;)r=r.replace(" "+o+" "," ");a=se.trim(r),i!==a&&n.setAttribute("class",a)}return this},toggleClass:function(e,t){var n=typeof e;return"boolean"==typeof t&&"string"===n?t?this.addClass(e):this.removeClass(e):se.isFunction(e)?this.each(function(n){se(this).toggleClass(e.call(this,n,B(this),t),t)}):this.each(function(){var t,r,i,o;if("string"===n)for(r=0,i=se(this),o=e.match(Ce)||[];t=o[r++];)i.hasClass(t)?i.removeClass(t):i.addClass(t);else void 0!==e&&"boolean"!==n||(t=B(this),t&&Se.set(this,"__className__",t),this.setAttribute&&this.setAttribute("class",t||e===!1?"":Se.get(this,"__className__")||""))})},hasClass:function(e){var t,n,r=0;for(t=" "+e+" ";n=this[r++];)if(1===n.nodeType&&(" "+B(n)+" ").replace(dt," ").indexOf(t)>-1)return!0;return!1}});var ht=/\r/g,gt=/[\x20\t\r\n\f]+/g;se.fn.extend({val:function(e){var t,n,r,i=this[0];{if(arguments.length)return r=se.isFunction(e),this.each(function(n){var i;1===this.nodeType&&(i=r?e.call(this,n,se(this).val()):e,null==i?i="":"number"==typeof i?i+="":se.isArray(i)&&(i=se.map(i,function(e){return null==e?"":e+""})),t=se.valHooks[this.type]||se.valHooks[this.nodeName.toLowerCase()],t&&"set"in t&&void 0!==t.set(this,i,"value")||(this.value=i))});if(i)return t=se.valHooks[i.type]||se.valHooks[i.nodeName.toLowerCase()],t&&"get"in t&&void 0!==(n=t.get(i,"value"))?n:(n=i.value,"string"==typeof n?n.replace(ht,""):null==n?"":n)}}}),se.extend({valHooks:{option:{get:function(e){var t=se.find.attr(e,"value");return null!=t?t:se.trim(se.text(e)).replace(gt," ")}},select:{get:function(e){for(var t,n,r=e.options,i=e.selectedIndex,o="select-one"===e.type||i<0,s=o?null:[],a=o?i+1:r.length,u=i<0?a:o?i:0;u-1)&&(n=!0);return n||(e.selectedIndex=-1),o}}}}),se.each(["radio","checkbox"],function(){se.valHooks[this]={set:function(e,t){if(se.isArray(t))return e.checked=se.inArray(se(e).val(),t)>-1}},ie.checkOn||(se.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})});var yt=/^(?:focusinfocus|focusoutblur)$/;se.extend(se.event,{trigger:function(e,n,r,i){var o,s,a,u,l,c,f,p=[r||G],d=re.call(e,"type")?e.type:e,h=re.call(e,"namespace")?e.namespace.split("."):[];if(s=a=r=r||G,3!==r.nodeType&&8!==r.nodeType&&!yt.test(d+se.event.triggered)&&(d.indexOf(".")>-1&&(h=d.split("."),d=h.shift(),h.sort()),l=d.indexOf(":")<0&&"on"+d,e=e[se.expando]?e:new se.Event(d,"object"==typeof e&&e),e.isTrigger=i?2:3,e.namespace=h.join("."),e.rnamespace=e.namespace?new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,e.result=void 0,e.target||(e.target=r),n=null==n?[e]:se.makeArray(n,[e]),f=se.event.special[d]||{},i||!f.trigger||f.trigger.apply(r,n)!==!1)){if(!i&&!f.noBubble&&!se.isWindow(r)){for(u=f.delegateType||d,yt.test(u+d)||(s=s.parentNode);s;s=s.parentNode)p.push(s),a=s;a===(r.ownerDocument||G)&&p.push(a.defaultView||a.parentWindow||t)}for(o=0;(s=p[o++])&&!e.isPropagationStopped();)e.type=o>1?u:f.bindType||d,c=(Se.get(s,"events")||{})[e.type]&&Se.get(s,"handle"),c&&c.apply(s,n),c=l&&s[l],c&&c.apply&&Ee(s)&&(e.result=c.apply(s,n),e.result===!1&&e.preventDefault());return e.type=d,i||e.isDefaultPrevented()||f._default&&f._default.apply(p.pop(),n)!==!1||!Ee(r)||l&&se.isFunction(r[d])&&!se.isWindow(r)&&(a=r[l],a&&(r[l]=null),se.event.triggered=d,r[d](),se.event.triggered=void 0,a&&(r[l]=a)),e.result}},simulate:function(e,t,n){var r=se.extend(new se.Event,n,{type:e,isSimulated:!0});se.event.trigger(r,null,t)}}),se.fn.extend({trigger:function(e,t){return this.each(function(){se.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];if(n)return se.event.trigger(e,t,n,!0)}}),se.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(e,t){se.fn[t]=function(e,n){return arguments.length>0?this.on(t,null,e,n):this.trigger(t)}}),se.fn.extend({hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),ie.focusin="onfocusin"in t,ie.focusin||se.each({focus:"focusin",blur:"focusout"},function(e,t){var n=function(e){se.event.simulate(t,e.target,se.event.fix(e))};se.event.special[t]={setup:function(){var r=this.ownerDocument||this,i=Se.access(r,t);i||r.addEventListener(e,n,!0),Se.access(r,t,(i||0)+1)},teardown:function(){var r=this.ownerDocument||this,i=Se.access(r,t)-1;i?Se.access(r,t,i):(r.removeEventListener(e,n,!0),Se.remove(r,t))}}});var vt=t.location,mt=se.now(),bt=/\?/;se.parseJSON=function(e){return JSON.parse(e+"")},se.parseXML=function(e){var n;if(!e||"string"!=typeof e)return null;try{n=(new t.DOMParser).parseFromString(e,"text/xml")}catch(r){n=void 0}return n&&!n.getElementsByTagName("parsererror").length||se.error("Invalid XML: "+e),n};var xt=/#.*$/,wt=/([?&])_=[^&]*/,Ct=/^(.*?):[ \t]*([^\r\n]*)$/gm,kt=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Tt=/^(?:GET|HEAD)$/,Et=/^\/\//,St={},Nt={},jt="*/".concat("*"),Dt=G.createElement("a");Dt.href=vt.href,se.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:vt.href,type:"GET",isLocal:kt.test(vt.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":jt,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":se.parseJSON,"text xml":se.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?V(V(e,se.ajaxSettings),t):V(se.ajaxSettings,e)},ajaxPrefilter:W(St),ajaxTransport:W(Nt),ajax:function(e,n){function r(e,n,r,a){var l,f,m,b,w,k=n;2!==x&&(x=2,u&&t.clearTimeout(u),i=void 0,s=a||"",C.readyState=e>0?4:0,l=e>=200&&e<300||304===e,r&&(b=X(p,C,r)),b=z(p,b,C,l),l?(p.ifModified&&(w=C.getResponseHeader("Last-Modified"),w&&(se.lastModified[o]=w),w=C.getResponseHeader("etag"),w&&(se.etag[o]=w)),204===e||"HEAD"===p.type?k="nocontent":304===e?k="notmodified":(k=b.state,f=b.data,m=b.error,l=!m)):(m=k,!e&&k||(k="error",e<0&&(e=0))),C.status=e,C.statusText=(n||k)+"",l?g.resolveWith(d,[f,k,C]):g.rejectWith(d,[C,k,m]),C.statusCode(v),v=void 0,c&&h.trigger(l?"ajaxSuccess":"ajaxError",[C,p,l?f:m]),y.fireWith(d,[C,k]),c&&(h.trigger("ajaxComplete",[C,p]),--se.active||se.event.trigger("ajaxStop")))}"object"==typeof e&&(n=e,e=void 0),n=n||{};var i,o,s,a,u,l,c,f,p=se.ajaxSetup({},n),d=p.context||p,h=p.context&&(d.nodeType||d.jquery)?se(d):se.event,g=se.Deferred(),y=se.Callbacks("once memory"),v=p.statusCode||{},m={},b={},x=0,w="canceled",C={readyState:0,getResponseHeader:function(e){var t;if(2===x){if(!a)for(a={};t=Ct.exec(s);)a[t[1].toLowerCase()]=t[2];t=a[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return 2===x?s:null},setRequestHeader:function(e,t){var n=e.toLowerCase();return x||(e=b[n]=b[n]||e,m[e]=t),this},overrideMimeType:function(e){return x||(p.mimeType=e),this},statusCode:function(e){var t;if(e)if(x<2)for(t in e)v[t]=[v[t],e[t]];else C.always(e[C.status]);return this},abort:function(e){var t=e||w;return i&&i.abort(t),r(0,t),this}};if(g.promise(C).complete=y.add,C.success=C.done,C.error=C.fail,p.url=((e||p.url||vt.href)+"").replace(xt,"").replace(Et,vt.protocol+"//"),p.type=n.method||n.type||p.method||p.type,p.dataTypes=se.trim(p.dataType||"*").toLowerCase().match(Ce)||[""],null==p.crossDomain){l=G.createElement("a");try{l.href=p.url,l.href=l.href,p.crossDomain=Dt.protocol+"//"+Dt.host!=l.protocol+"//"+l.host}catch(k){p.crossDomain=!0}}if(p.data&&p.processData&&"string"!=typeof p.data&&(p.data=se.param(p.data,p.traditional)),$(St,p,n,C),2===x)return C;c=se.event&&p.global,c&&0===se.active++&&se.event.trigger("ajaxStart"),p.type=p.type.toUpperCase(),p.hasContent=!Tt.test(p.type),o=p.url,p.hasContent||(p.data&&(o=p.url+=(bt.test(o)?"&":"?")+p.data,delete p.data),p.cache===!1&&(p.url=wt.test(o)?o.replace(wt,"$1_="+mt++):o+(bt.test(o)?"&":"?")+"_="+mt++)),p.ifModified&&(se.lastModified[o]&&C.setRequestHeader("If-Modified-Since",se.lastModified[o]),se.etag[o]&&C.setRequestHeader("If-None-Match",se.etag[o])),(p.data&&p.hasContent&&p.contentType!==!1||n.contentType)&&C.setRequestHeader("Content-Type",p.contentType),C.setRequestHeader("Accept",p.dataTypes[0]&&p.accepts[p.dataTypes[0]]?p.accepts[p.dataTypes[0]]+("*"!==p.dataTypes[0]?", "+jt+"; q=0.01":""):p.accepts["*"]);for(f in p.headers)C.setRequestHeader(f,p.headers[f]);if(p.beforeSend&&(p.beforeSend.call(d,C,p)===!1||2===x))return C.abort();w="abort";for(f in{success:1,error:1,complete:1})C[f](p[f]);if(i=$(Nt,p,n,C)){if(C.readyState=1,c&&h.trigger("ajaxSend",[C,p]),2===x)return C;p.async&&p.timeout>0&&(u=t.setTimeout(function(){C.abort("timeout")},p.timeout));try{x=1,i.send(m,r)}catch(k){if(!(x<2))throw k;r(-1,k)}}else r(-1,"No Transport");return C},getJSON:function(e,t,n){return se.get(e,t,n,"json")},getScript:function(e,t){return se.get(e,void 0,t,"script")}}),se.each(["get","post"],function(e,t){se[t]=function(e,n,r,i){return se.isFunction(n)&&(i=i||r,r=n,n=void 0),se.ajax(se.extend({url:e,type:t,dataType:i,data:n,success:r},se.isPlainObject(e)&&e))}}),se._evalUrl=function(e){return se.ajax({url:e,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})},se.fn.extend({wrapAll:function(e){var t;return se.isFunction(e)?this.each(function(t){se(this).wrapAll(e.call(this,t))}):(this[0]&&(t=se(e,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){for(var e=this;e.firstElementChild;)e=e.firstElementChild;return e}).append(this)),this)},wrapInner:function(e){return se.isFunction(e)?this.each(function(t){se(this).wrapInner(e.call(this,t))}):this.each(function(){var t=se(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=se.isFunction(e);return this.each(function(n){se(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(){return this.parent().each(function(){se.nodeName(this,"body")||se(this).replaceWith(this.childNodes)}).end()}}),se.expr.filters.hidden=function(e){return!se.expr.filters.visible(e)},se.expr.filters.visible=function(e){return e.offsetWidth>0||e.offsetHeight>0||e.getClientRects().length>0};var At=/%20/g,qt=/\[\]$/,Lt=/\r?\n/g,Ot=/^(?:submit|button|image|reset|file)$/i,_t=/^(?:input|select|textarea|keygen)/i;se.param=function(e,t){var n,r=[],i=function(e,t){t=se.isFunction(t)?t():null==t?"":t,r[r.length]=encodeURIComponent(e)+"="+encodeURIComponent(t)};if(void 0===t&&(t=se.ajaxSettings&&se.ajaxSettings.traditional),se.isArray(e)||e.jquery&&!se.isPlainObject(e))se.each(e,function(){i(this.name,this.value)});else for(n in e)U(n,e[n],t,i);return r.join("&").replace(At,"+")},se.fn.extend({serialize:function(){return se.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=se.prop(this,"elements");return e?se.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!se(this).is(":disabled")&&_t.test(this.nodeName)&&!Ot.test(e)&&(this.checked||!_e.test(e))}).map(function(e,t){var n=se(this).val();return null==n?null:se.isArray(n)?se.map(n,function(e){return{name:t.name,value:e.replace(Lt,"\r\n")}}):{name:t.name,value:n.replace(Lt,"\r\n")}}).get()}}),se.ajaxSettings.xhr=function(){try{return new t.XMLHttpRequest}catch(e){}};var Ht={0:200,1223:204},Pt=se.ajaxSettings.xhr();ie.cors=!!Pt&&"withCredentials"in Pt,ie.ajax=Pt=!!Pt,se.ajaxTransport(function(e){var n,r;if(ie.cors||Pt&&!e.crossDomain)return{send:function(i,o){var s,a=e.xhr();if(a.open(e.type,e.url,e.async,e.username,e.password),e.xhrFields)for(s in e.xhrFields)a[s]=e.xhrFields[s];e.mimeType&&a.overrideMimeType&&a.overrideMimeType(e.mimeType),e.crossDomain||i["X-Requested-With"]||(i["X-Requested-With"]="XMLHttpRequest");for(s in i)a.setRequestHeader(s,i[s]);n=function(e){return function(){n&&(n=r=a.onload=a.onerror=a.onabort=a.onreadystatechange=null,"abort"===e?a.abort():"error"===e?"number"!=typeof a.status?o(0,"error"):o(a.status,a.statusText):o(Ht[a.status]||a.status,a.statusText,"text"!==(a.responseType||"text")||"string"!=typeof a.responseText?{binary:a.response}:{text:a.responseText},a.getAllResponseHeaders()))}},a.onload=n(),r=a.onerror=n("error"),void 0!==a.onabort?a.onabort=r:a.onreadystatechange=function(){4===a.readyState&&t.setTimeout(function(){n&&r()})},n=n("abort");try{a.send(e.hasContent&&e.data||null)}catch(u){if(n)throw u}},abort:function(){n&&n()}}}),se.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(e){return se.globalEval(e),e}}}),se.ajaxPrefilter("script",function(e){void 0===e.cache&&(e.cache=!1),e.crossDomain&&(e.type="GET")}),se.ajaxTransport("script",function(e){if(e.crossDomain){var t,n;return{send:function(r,i){t=se(" 10 | 11 | 12 | 13 |
14 |

React-Spreadsheet-Component

15 |

These are two rather simple examples of React-Spreadsheet-Component, a simple spreadsheet component in React (creative name, huh?). It's made with <3 by Microsoft DX and released under the MIT License.

16 |
17 |
18 |

Simple Example Spreadsheet

19 |
20 |
21 |
22 |

Fancy Editable Spreadsheet

23 |
24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var browserify = require('browserify'); 2 | var del = require('del'); 3 | var gulp = require('gulp'); 4 | var source = require('vinyl-source-stream'); 5 | 6 | var header = require('gulp-header'); 7 | var jshint = require('gulp-jshint'); 8 | var rename = require('gulp-rename'); 9 | var plumber = require('gulp-plumber'); 10 | var react = require('gulp-react'); 11 | var streamify = require('gulp-streamify'); 12 | var uglify = require('gulp-uglify'); 13 | var gutil = require('gulp-util'); 14 | var connect = require('gulp-connect'); 15 | var babel = require('gulp-babel'); 16 | var babelify = require('babelify'); 17 | 18 | var pkg = require('./package.json'); 19 | var devBuild = (process.env.NODE_ENV === 'production') ? '' : ' (dev build at ' + (new Date()).toUTCString() + ')'; 20 | var distHeader = '/*!\n\ 21 | * <%= pkg.name %> <%= pkg.version %><%= devBuild %> - <%= pkg.homepage %>\n\ 22 | * <%= pkg.license %> Licensed\n\ 23 | */\n'; 24 | 25 | var jsSrcPaths = './src/*.js*' 26 | var jsLibPaths = './lib/*.js' 27 | 28 | gulp.task('clean-lib', function (cb) { 29 | del(jsLibPaths).then(function () { 30 | cb(); 31 | }); 32 | }); 33 | 34 | gulp.task('transpile-js', ['clean-lib'], function () { 35 | return gulp.src(jsSrcPaths) 36 | .pipe(plumber()) 37 | .pipe(react({harmony: false, es6module: true})) 38 | .pipe(babel()) 39 | .pipe(gulp.dest('./lib')); 40 | }); 41 | 42 | gulp.task('lint-js', ['transpile-js'], function () { 43 | return gulp.src(jsLibPaths) 44 | .pipe(jshint('./.jshintrc')) 45 | .pipe(jshint.reporter('jshint-stylish')); 46 | }); 47 | 48 | gulp.task('bundle-js', ['lint-js'], function () { 49 | var b = browserify(pkg.main, { 50 | debug: !!gutil.env.debug 51 | , standalone: pkg.standalone 52 | , detectGlobals: false 53 | }); 54 | 55 | b.transform('browserify-shim') 56 | 57 | var stream = b.bundle() 58 | .pipe(source('spreadsheet.js')) 59 | .pipe(streamify(header(distHeader, { pkg: pkg, devBuild: devBuild }))) 60 | .pipe(gulp.dest('./dist')); 61 | 62 | if (process.env.NODE_ENV === 'production') { 63 | stream = stream 64 | .pipe(rename('spreadsheet.min.js')) 65 | .pipe(streamify(uglify())) 66 | .pipe(streamify(header(distHeader, { pkg: pkg, devBuild: devBuild }))) 67 | .pipe(gulp.dest('./dist')); 68 | } 69 | 70 | return stream; 71 | }); 72 | 73 | gulp.task('watch', function () { 74 | gulp.watch(jsSrcPaths, ['bundle-js']); 75 | }); 76 | 77 | gulp.task('connect', function () { 78 | connect.server(); 79 | 80 | gutil.log('--------------------------------------------') 81 | gutil.log(gutil.colors.magenta('To see the example, open up a browser and go')); 82 | gutil.log(gutil.colors.bold.red('to http://localhost:8080/example')); 83 | gutil.log('--------------------------------------------'); 84 | }); 85 | 86 | gulp.task('example', ['transpile-js'], function () { 87 | return browserify('./example.js') 88 | .transform("babelify", {presets: ["es2015", "react"]}) 89 | .bundle() 90 | .pipe(source('bundle.js')) 91 | .pipe(gulp.dest('./example')); 92 | }); 93 | 94 | gulp.task('default', ['bundle-js', 'connect', 'watch']); 95 | -------------------------------------------------------------------------------- /lib/cell.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 4 | 5 | var _react = require('react'); 6 | 7 | var _react2 = _interopRequireDefault(_react); 8 | 9 | var _dispatcher = require('./dispatcher'); 10 | 11 | var _dispatcher2 = _interopRequireDefault(_dispatcher); 12 | 13 | var _helpers = require('./helpers'); 14 | 15 | var _helpers2 = _interopRequireDefault(_helpers); 16 | 17 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 18 | 19 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 20 | 21 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 22 | 23 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 24 | 25 | var CellComponent = function (_Component) { 26 | _inherits(CellComponent, _Component); 27 | 28 | function CellComponent(props) { 29 | _classCallCheck(this, CellComponent); 30 | 31 | var _this = _possibleConstructorReturn(this, (CellComponent.__proto__ || Object.getPrototypeOf(CellComponent)).call(this, props)); 32 | 33 | _this.state = { 34 | editing: _this.props.editing, 35 | changedValue: _this.props.value 36 | }; 37 | return _this; 38 | } 39 | 40 | /** 41 | * React "render" method, rendering the individual cell 42 | */ 43 | 44 | 45 | _createClass(CellComponent, [{ 46 | key: 'render', 47 | value: function render() { 48 | var props = this.props, 49 | selected = props.selected ? 'selected' : '', 50 | ref = 'input_' + props.uid.join('_'), 51 | config = props.config || { emptyValueSymbol: '' }, 52 | displayValue = props.value === '' || !props.value ? config.emptyValueSymbol : props.value, 53 | cellClasses = props.cellClasses && props.cellClasses.length > 0 ? props.cellClasses + ' ' + selected : selected, 54 | cellContent; 55 | 56 | // Check if header - if yes, render it 57 | var header = this.renderHeader(); 58 | if (header) { 59 | return header; 60 | } 61 | 62 | // If not a header, check for editing and return 63 | if (props.selected && props.editing) { 64 | cellContent = _react2.default.createElement("input", { className: "mousetrap", 65 | onChange: this.handleChange.bind(this), 66 | onBlur: this.handleBlur.bind(this), 67 | ref: ref, 68 | defaultValue: this.props.value }); 69 | } 70 | 71 | return _react2.default.createElement("td", { className: cellClasses, ref: props.uid.join('_') }, _react2.default.createElement("div", { className: "reactTableCell" }, cellContent, _react2.default.createElement("span", { onDoubleClick: this.handleDoubleClick.bind(this), onClick: this.handleClick.bind(this) }, displayValue))); 72 | } 73 | 74 | /** 75 | * React "componentDidUpdate" method, ensuring correct input focus 76 | * @param {React previous properties} prevProps 77 | * @param {React previous state} prevState 78 | */ 79 | 80 | }, { 81 | key: 'componentDidUpdate', 82 | value: function componentDidUpdate(prevProps, prevState) { 83 | if (this.props.editing && this.props.selected) { 84 | var node = this.refs['input_' + this.props.uid.join('_')]; 85 | node.focus(); 86 | } 87 | 88 | if (prevProps.selected && prevProps.editing && this.state.changedValue !== this.props.value) { 89 | this.props.onCellValueChange(this.props.uid, this.state.changedValue); 90 | } 91 | } 92 | 93 | /** 94 | * Click handler for individual cell, ensuring navigation and selection 95 | * @param {event} e 96 | */ 97 | 98 | }, { 99 | key: 'handleClick', 100 | value: function handleClick(e) { 101 | var cellElement = this.refs[this.props.uid.join('_')]; 102 | this.props.handleSelectCell(this.props.uid, cellElement); 103 | } 104 | 105 | /** 106 | * Click handler for individual cell if the cell is a header cell 107 | * @param {event} e 108 | */ 109 | 110 | }, { 111 | key: 'handleHeadClick', 112 | value: function handleHeadClick(e) { 113 | var cellElement = this.refs[this.props.uid.join('_')]; 114 | _dispatcher2.default.publish('headCellClicked', cellElement, this.props.spreadsheetId); 115 | } 116 | 117 | /** 118 | * Double click handler for individual cell, ensuring navigation and selection 119 | * @param {event} e 120 | */ 121 | 122 | }, { 123 | key: 'handleDoubleClick', 124 | value: function handleDoubleClick(e) { 125 | e.preventDefault(); 126 | this.props.handleDoubleClickOnCell(this.props.uid); 127 | } 128 | 129 | /** 130 | * Blur handler for individual cell 131 | * @param {event} e 132 | */ 133 | 134 | }, { 135 | key: 'handleBlur', 136 | value: function handleBlur(e) { 137 | var newValue = this.refs['input_' + this.props.uid.join('_')].value; 138 | 139 | this.props.onCellValueChange(this.props.uid, newValue, e); 140 | this.props.handleCellBlur(this.props.uid); 141 | _dispatcher2.default.publish('cellBlurred', this.props.uid, this.props.spreadsheetId); 142 | } 143 | 144 | /** 145 | * Change handler for an individual cell, propagating the value change 146 | * @param {event} e 147 | */ 148 | 149 | }, { 150 | key: 'handleChange', 151 | value: function handleChange(e) { 152 | var newValue = this.refs['input_' + this.props.uid.join('_')].value; 153 | this.setState({ changedValue: newValue }); 154 | } 155 | 156 | /** 157 | * Checks if a header exists - if it does, it returns a header object 158 | * @return {false|react} [Either false if it's not a header cell, a react object if it is] 159 | */ 160 | 161 | }, { 162 | key: 'renderHeader', 163 | value: function renderHeader() { 164 | var props = this.props, 165 | selected = props.selected ? 'selected' : '', 166 | uid = props.uid, 167 | config = props.config || { emptyValueSymbol: '' }, 168 | displayValue = props.value === '' || !props.value ? config.emptyValueSymbol : props.value, 169 | cellClasses = props.cellClasses && props.cellClasses.length > 0 ? this.props.cellClasses + ' ' + selected : selected; 170 | 171 | // Cases 172 | var headRow = uid[0] === 0, 173 | headColumn = uid[1] === 0, 174 | headRowAndEnabled = config.hasHeadRow && uid[0] === 0, 175 | headColumnAndEnabled = config.hasHeadColumn && uid[1] === 0; 176 | 177 | // Head Row enabled, cell is in head row 178 | // Head Column enabled, cell is in head column 179 | if (headRowAndEnabled || headColumnAndEnabled) { 180 | if (headColumn && config.hasLetterNumberHeads) { 181 | displayValue = uid[0]; 182 | } else if (headRow && config.hasLetterNumberHeads) { 183 | displayValue = _helpers2.default.countWithLetters(uid[1]); 184 | } 185 | 186 | if (config.isHeadRowString && headRow || config.isHeadColumnString && headColumn) { 187 | return _react2.default.createElement("th", { className: cellClasses, ref: this.props.uid.join('_') }, _react2.default.createElement("div", null, _react2.default.createElement("span", { onClick: this.handleHeadClick.bind(this) }, displayValue))); 188 | } else { 189 | return _react2.default.createElement("th", { ref: this.props.uid.join('_') }, displayValue); 190 | } 191 | } else { 192 | return false; 193 | } 194 | } 195 | }]); 196 | 197 | return CellComponent; 198 | }(_react.Component); 199 | 200 | module.exports = CellComponent; -------------------------------------------------------------------------------- /lib/dispatcher.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Mousetrap = require('mousetrap'); 4 | var $ = require('jquery'); 5 | 6 | var dispatcher = { 7 | // Event Pub/Sub System 8 | // 9 | // Topics used: 10 | // [headCellClicked] - A head cell was clicked 11 | // @return {array} [row, column] 12 | // [cellSelected] - A cell was selected 13 | // @return {array} [row, column] 14 | // [cellBlur] - A cell was blurred 15 | // @return {array} [row, column] 16 | // [cellValueChanged] - A cell value changed. 17 | // @return {cell, newValue} Origin cell, new value entered 18 | // [dataChanged] - Data changed 19 | // @return {data} New data 20 | // [editStarted] - The user started editing 21 | // @return {cell} Origin cell 22 | // [editStopped] - The user stopped editing 23 | // @return {cell} Origin cell 24 | // [rowCreated] - The user created a row 25 | // @return {number} Row index 26 | // [columnCreated] - The user created a column 27 | // @return {number} Column index 28 | topics: {}, 29 | 30 | /** 31 | * Subscribe to an event 32 | * @param {string} topic [The topic subscribing to] 33 | * @param {function} listener [The callback for published events] 34 | * @param {string} spreadsheetId [The reactId (data-spreadsheetId) of the origin element] 35 | */ 36 | subscribe: function subscribe(topic, listener, spreadsheetId) { 37 | if (!this.topics[spreadsheetId]) { 38 | this.topics[spreadsheetId] = []; 39 | } 40 | 41 | if (!this.topics[spreadsheetId][topic]) { 42 | this.topics[spreadsheetId][topic] = []; 43 | } 44 | 45 | this.topics[spreadsheetId][topic].push(listener); 46 | }, 47 | 48 | /** 49 | * Publish to an event channel 50 | * @param {string} topic [The topic publishing to] 51 | * @param {object} data [An object passed to the subscribed callbacks] 52 | * @param {string} spreadsheetId [The reactId (data-spreadsheetId) of the origin element] 53 | */ 54 | publish: function publish(topic, data, spreadsheetId) { 55 | // return if the topic doesn't exist, or there are no listeners 56 | if (!this.topics[spreadsheetId] || !this.topics[spreadsheetId][topic] || this.topics[spreadsheetId][topic].length < 1) { 57 | return; 58 | } 59 | 60 | this.topics[spreadsheetId][topic].forEach(function (listener) { 61 | listener(data || {}); 62 | }); 63 | }, 64 | 65 | keyboardShortcuts: [ 66 | // Name, Keys, Events 67 | ['down', 'down', ['keyup']], ['up', 'up', ['keyup']], ['left', 'left', ['keyup']], ['right', 'right', ['keyup']], ['tab', 'tab', ['keyup', 'keydown']], ['enter', 'enter', ['keyup']], ['esc', 'esc', ['keyup']], ['remove', ['backspace', 'delete'], ['keyup', 'keydown']], ['letter', ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'x', 'w', 'y', 'z', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '=', '.', ',', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'X', 'W', 'Y', 'Z'], ['keyup', 'keydown']]], 68 | 69 | /** 70 | * Initializes the keyboard bindings 71 | * @param {object} domNode [The DOM node of the element that should be bound] 72 | * @param {string} spreadsheetId [The id of the spreadsheet element] 73 | */ 74 | setupKeyboardShortcuts: function setupKeyboardShortcuts(domNode, spreadsheetId) { 75 | var self = this; 76 | this.keyboardShortcuts.map(function (shortcut) { 77 | var shortcutName = shortcut[0], 78 | shortcutKey = shortcut[1], 79 | events = shortcut[2]; 80 | 81 | events.map(function (event) { 82 | Mousetrap(domNode).bind(shortcutKey, function (e) { 83 | self.publish(shortcutName + '_' + event, e, spreadsheetId); 84 | }, event); 85 | }); 86 | }); 87 | 88 | // Avoid scroll 89 | window.addEventListener('keydown', function (e) { 90 | // space and arrow keys 91 | if ([32, 37, 38, 39, 40].indexOf(e.keyCode) > -1 && $(document.activeElement)[0].tagName !== 'INPUT') { 92 | if (e.preventDefault) { 93 | e.preventDefault(); 94 | } else { 95 | // Oh, old IE, you 💩 96 | e.returnValue = false; 97 | } 98 | } 99 | }, false); 100 | } 101 | }; 102 | 103 | module.exports = dispatcher; -------------------------------------------------------------------------------- /lib/helpers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Helpers = { 4 | /** 5 | * Find the first element in an array matching a boolean 6 | * @param {[array]} arr [Array to test] 7 | * @param {[function]} test [Test Function] 8 | * @param {[type]} context [Context] 9 | * @return {[object]} [Found element] 10 | */ 11 | firstInArray: function firstInArray(arr, test, context) { 12 | var result = null; 13 | 14 | arr.some(function (el, i) { 15 | return test.call(context, el, i, arr) ? (result = el, true) : false; 16 | }); 17 | 18 | return result; 19 | }, 20 | 21 | /** 22 | * Find the first TD in a path array 23 | * @param {[array]} arr [Path array containing elements] 24 | * @return {[object]} [Found element] 25 | */ 26 | firstTDinArray: function firstTDinArray(arr) { 27 | var cell = Helpers.firstInArray(arr, function (element) { 28 | if (element.nodeName && element.nodeName === 'TD') { 29 | return true; 30 | } else { 31 | return false; 32 | } 33 | }); 34 | 35 | return cell; 36 | }, 37 | 38 | /** 39 | * Check if two cell objects reference the same cell 40 | * @param {[array]} cell1 [First cell] 41 | * @param {[array]} cell2 [Second cell] 42 | * @return {[boolean]} [Boolean indicating if the cells are equal] 43 | */ 44 | equalCells: function equalCells(cell1, cell2) { 45 | if (!cell1 || !cell2 || cell1.length !== cell2.length) { 46 | return false; 47 | } 48 | 49 | if (cell1[0] === cell2[0] && cell1[1] === cell2[1]) { 50 | return true; 51 | } else { 52 | return false; 53 | } 54 | }, 55 | 56 | /** 57 | * Counts in letters (A, B, C...Z, AA); 58 | * @return {[string]} [Letter] 59 | */ 60 | countWithLetters: function countWithLetters(num) { 61 | var mod = num % 26, 62 | pow = num / 26 | 0, 63 | out = mod ? String.fromCharCode(64 + mod) : (--pow, 'Z'); 64 | return pow ? this.countWithLetters(pow) + out : out; 65 | }, 66 | 67 | /** 68 | * Creates a random 5-character id 69 | * @return {string} [Somewhat random id] 70 | */ 71 | makeSpreadsheetId: function makeSpreadsheetId() { 72 | var text = '', 73 | possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 74 | 75 | for (var i = 0; i < 5; i = i + 1) { 76 | text += possible.charAt(Math.floor(Math.random() * possible.length)); 77 | } 78 | 79 | return text; 80 | } 81 | }; 82 | 83 | module.exports = Helpers; -------------------------------------------------------------------------------- /lib/row.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 4 | 5 | var _react = require('react'); 6 | 7 | var _react2 = _interopRequireDefault(_react); 8 | 9 | var _cell = require('./cell'); 10 | 11 | var _cell2 = _interopRequireDefault(_cell); 12 | 13 | var _helpers = require('./helpers'); 14 | 15 | var _helpers2 = _interopRequireDefault(_helpers); 16 | 17 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 18 | 19 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 20 | 21 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 22 | 23 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 24 | 25 | var RowComponent = function (_Component) { 26 | _inherits(RowComponent, _Component); 27 | 28 | function RowComponent() { 29 | _classCallCheck(this, RowComponent); 30 | 31 | return _possibleConstructorReturn(this, (RowComponent.__proto__ || Object.getPrototypeOf(RowComponent)).apply(this, arguments)); 32 | } 33 | 34 | _createClass(RowComponent, [{ 35 | key: 'render', 36 | 37 | /** 38 | * React Render method 39 | * @return {[JSX]} [JSX to render] 40 | */ 41 | value: function render() { 42 | var config = this.props.config, 43 | cells = this.props.cells, 44 | columns = [], 45 | key, 46 | uid, 47 | selected, 48 | cellClasses, 49 | i; 50 | 51 | if (!config.columns || cells.length === 0) { 52 | return console.error('Table can\'t be initialized without set number of columsn and no data!'); 53 | } 54 | 55 | for (i = 0; i < cells.length; i = i + 1) { 56 | // If a cell is selected, check if it's this one 57 | selected = _helpers2.default.equalCells(this.props.selected, [this.props.uid, i]); 58 | cellClasses = this.props.cellClasses && this.props.cellClasses[i] ? this.props.cellClasses[i] : ''; 59 | 60 | key = 'row_' + this.props.uid + '_cell_' + i; 61 | uid = [this.props.uid, i]; 62 | columns.push(_react2.default.createElement(_cell2.default, { key: key, 63 | uid: uid, 64 | value: cells[i], 65 | config: config, 66 | cellClasses: cellClasses, 67 | onCellValueChange: this.props.onCellValueChange, 68 | handleSelectCell: this.props.handleSelectCell, 69 | handleDoubleClickOnCell: this.props.handleDoubleClickOnCell, 70 | handleCellBlur: this.props.handleCellBlur, 71 | spreadsheetId: this.props.spreadsheetId, 72 | selected: selected, 73 | editing: this.props.editing })); 74 | } 75 | 76 | return _react2.default.createElement("tr", null, columns); 77 | } 78 | }]); 79 | 80 | return RowComponent; 81 | }(_react.Component); 82 | 83 | module.exports = RowComponent; -------------------------------------------------------------------------------- /lib/spreadsheet.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 4 | 5 | var _react = require('react'); 6 | 7 | var _react2 = _interopRequireDefault(_react); 8 | 9 | var _row = require('./row'); 10 | 11 | var _row2 = _interopRequireDefault(_row); 12 | 13 | var _dispatcher = require('./dispatcher'); 14 | 15 | var _dispatcher2 = _interopRequireDefault(_dispatcher); 16 | 17 | var _helpers = require('./helpers'); 18 | 19 | var _helpers2 = _interopRequireDefault(_helpers); 20 | 21 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 22 | 23 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 24 | 25 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 26 | 27 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 28 | 29 | var $ = require('jquery'); 30 | 31 | var SpreadsheetComponent = function (_Component) { 32 | _inherits(SpreadsheetComponent, _Component); 33 | 34 | function SpreadsheetComponent(props) { 35 | _classCallCheck(this, SpreadsheetComponent); 36 | 37 | var _this = _possibleConstructorReturn(this, (SpreadsheetComponent.__proto__ || Object.getPrototypeOf(SpreadsheetComponent)).call(this, props)); 38 | 39 | var initialData = _this.props.initialData || {}; 40 | 41 | if (!initialData.rows) { 42 | initialData.rows = []; 43 | 44 | for (var i = 0; i < _this.props.config.rows; i = i + 1) { 45 | initialData.rows[i] = []; 46 | for (var ci = 0; ci < _this.props.config.columns; ci = ci + 1) { 47 | initialData.rows[i][ci] = ''; 48 | } 49 | } 50 | } 51 | 52 | _this.state = { 53 | data: initialData, 54 | selected: null, 55 | lastBlurred: null, 56 | selectedElement: null, 57 | editing: false, 58 | id: _this.props.spreadsheetId || _helpers2.default.makeSpreadsheetId() 59 | }; 60 | return _this; 61 | } 62 | 63 | /** 64 | * React 'componentDidMount' method 65 | */ 66 | 67 | 68 | _createClass(SpreadsheetComponent, [{ 69 | key: 'componentDidMount', 70 | value: function componentDidMount() { 71 | this.bindKeyboard(); 72 | 73 | $('body').on('focus', 'input', function (e) { 74 | $(this).one('mouseup', function () { 75 | $(this).select(); 76 | return false; 77 | }).select(); 78 | }); 79 | } 80 | 81 | /** 82 | * React Render method 83 | * @return {[JSX]} [JSX to render] 84 | */ 85 | 86 | }, { 87 | key: 'render', 88 | value: function render() { 89 | var data = this.state.data, 90 | config = this.props.config, 91 | _cellClasses = this.props.cellClasses, 92 | rows = [], 93 | key, 94 | i, 95 | cellClasses; 96 | 97 | // Sanity checks 98 | if (!data.rows && !config.rows) { 99 | return console.error('Table Component: Number of colums not defined in both data and config!'); 100 | } 101 | 102 | // Create Rows 103 | for (i = 0; i < data.rows.length; i = i + 1) { 104 | key = 'row_' + i; 105 | cellClasses = _cellClasses && _cellClasses.rows && _cellClasses.rows[i] ? _cellClasses.rows[i] : null; 106 | 107 | rows.push(_react2.default.createElement(_row2.default, { cells: data.rows[i], 108 | cellClasses: cellClasses, 109 | uid: i, 110 | key: key, 111 | config: config, 112 | selected: this.state.selected, 113 | editing: this.state.editing, 114 | handleSelectCell: this.handleSelectCell.bind(this), 115 | handleDoubleClickOnCell: this.handleDoubleClickOnCell.bind(this), 116 | handleCellBlur: this.handleCellBlur.bind(this), 117 | onCellValueChange: this.handleCellValueChange.bind(this), 118 | spreadsheetId: this.state.id, 119 | className: "cellComponent" })); 120 | } 121 | 122 | return _react2.default.createElement("table", { tabIndex: "0", "data-spreasheet-id": this.state.id, ref: "react-spreadsheet-" + this.state.id }, _react2.default.createElement("tbody", null, rows)); 123 | } 124 | 125 | /** 126 | * Binds the various keyboard events dispatched to table functions 127 | */ 128 | 129 | }, { 130 | key: 'bindKeyboard', 131 | value: function bindKeyboard() { 132 | var _this2 = this; 133 | 134 | _dispatcher2.default.setupKeyboardShortcuts($(this.refs["react-spreadsheet-" + this.state.id])[0], this.state.id); 135 | 136 | _dispatcher2.default.subscribe('up_keyup', function (data) { 137 | _this2.navigateTable('up', data); 138 | }, this.state.id); 139 | _dispatcher2.default.subscribe('down_keyup', function (data) { 140 | _this2.navigateTable('down', data); 141 | }, this.state.id); 142 | _dispatcher2.default.subscribe('left_keyup', function (data) { 143 | _this2.navigateTable('left', data); 144 | }, this.state.id); 145 | _dispatcher2.default.subscribe('right_keyup', function (data) { 146 | _this2.navigateTable('right', data); 147 | }, this.state.id); 148 | _dispatcher2.default.subscribe('tab_keyup', function (data) { 149 | _this2.navigateTable('right', data, null, true); 150 | }, this.state.id); 151 | 152 | // Prevent brower's from jumping to URL bar 153 | _dispatcher2.default.subscribe('tab_keydown', function (data) { 154 | if ($(document.activeElement) && $(document.activeElement)[0].tagName === 'INPUT') { 155 | if (data.preventDefault) { 156 | data.preventDefault(); 157 | } else { 158 | // Oh, old IE, you 💩 159 | data.returnValue = false; 160 | } 161 | } 162 | }, this.state.id); 163 | 164 | _dispatcher2.default.subscribe('remove_keydown', function (data) { 165 | if (!$(data.target).is('input, textarea')) { 166 | if (data.preventDefault) { 167 | data.preventDefault(); 168 | } else { 169 | // Oh, old IE, you 💩 170 | data.returnValue = false; 171 | } 172 | } 173 | }, this.state.id); 174 | 175 | _dispatcher2.default.subscribe('enter_keyup', function () { 176 | if (_this2.state.selectedElement) { 177 | _this2.setState({ editing: !_this2.state.editing }); 178 | } 179 | $(_this2.refs["react-spreadsheet-" + _this2.state.id]).first().focus(); 180 | }, this.state.id); 181 | 182 | // Go into edit mode when the user starts typing on a field 183 | _dispatcher2.default.subscribe('letter_keydown', function () { 184 | if (!_this2.state.editing && _this2.state.selectedElement) { 185 | _dispatcher2.default.publish('editStarted', _this2.state.selectedElement, _this2.state.id); 186 | _this2.setState({ editing: true }); 187 | } 188 | }, this.state.id); 189 | 190 | // Delete on backspace and delete 191 | _dispatcher2.default.subscribe('remove_keyup', function () { 192 | if (_this2.state.selected && !_helpers2.default.equalCells(_this2.state.selected, _this2.state.lastBlurred)) { 193 | _this2.handleCellValueChange(_this2.state.selected, ''); 194 | } 195 | }, this.state.id); 196 | } 197 | 198 | /** 199 | * Navigates the table and moves selection 200 | * @param {string} direction [Direction ('up' || 'down' || 'left' || 'right')] 201 | * @param {Array: [number: row, number: cell]} originCell [Origin Cell] 202 | * @param {boolean} inEdit [Currently editing] 203 | */ 204 | 205 | }, { 206 | key: 'navigateTable', 207 | value: function navigateTable(direction, data, originCell, inEdit) { 208 | // Only traverse the table if the user isn't editing a cell, 209 | // unless override is given 210 | if (!inEdit && this.state.editing) { 211 | return false; 212 | } 213 | 214 | // Use the curently active cell if one isn't passed 215 | if (!originCell) { 216 | originCell = this.state.selectedElement; 217 | } 218 | 219 | // Prevent default 220 | if (data.preventDefault) { 221 | data.preventDefault(); 222 | } else { 223 | // Oh, old IE, you 💩 224 | data.returnValue = false; 225 | } 226 | 227 | var $origin = $(originCell), 228 | cellIndex = $origin.index(), 229 | target; 230 | 231 | if (direction === 'up') { 232 | target = $origin.closest('tr').prev().children().eq(cellIndex).find('span'); 233 | } else if (direction === 'down') { 234 | target = $origin.closest('tr').next().children().eq(cellIndex).find('span'); 235 | } else if (direction === 'left') { 236 | target = $origin.closest('td').prev().find('span'); 237 | } else if (direction === 'right') { 238 | target = $origin.closest('td').next().find('span'); 239 | } 240 | 241 | if (target.length > 0) { 242 | target.click(); 243 | } else { 244 | this.extendTable(direction, originCell); 245 | } 246 | } 247 | 248 | /** 249 | * Extends the table with an additional row/column, if permitted by config 250 | * @param {string} direction [Direction ('up' || 'down' || 'left' || 'right')] 251 | */ 252 | 253 | }, { 254 | key: 'extendTable', 255 | value: function extendTable(direction) { 256 | var config = this.props.config, 257 | data = this.state.data, 258 | newRow, 259 | i; 260 | 261 | if (direction === 'down' && config.canAddRow) { 262 | newRow = []; 263 | 264 | for (i = 0; i < this.state.data.rows[0].length; i = i + 1) { 265 | newRow[i] = ''; 266 | } 267 | 268 | data.rows.push(newRow); 269 | _dispatcher2.default.publish('rowCreated', data.rows.length, this.state.id); 270 | return this.setState({ data: data }); 271 | } 272 | 273 | if (direction === 'right' && config.canAddColumn) { 274 | for (i = 0; i < data.rows.length; i = i + 1) { 275 | data.rows[i].push(''); 276 | } 277 | 278 | _dispatcher2.default.publish('columnCreated', data.rows[0].length, this.state.id); 279 | return this.setState({ data: data }); 280 | } 281 | } 282 | 283 | /** 284 | * Callback for 'selectCell', updating the selected Cell 285 | * @param {Array: [number: row, number: cell]} cell [Selected Cell] 286 | * @param {object} cellElement [Selected Cell Element] 287 | */ 288 | 289 | }, { 290 | key: 'handleSelectCell', 291 | value: function handleSelectCell(cell, cellElement) { 292 | _dispatcher2.default.publish('cellSelected', cell, this.state.id); 293 | $(this.refs["react-spreadsheet-" + this.state.id]).first().focus(); 294 | 295 | this.setState({ 296 | selected: cell, 297 | selectedElement: cellElement 298 | }); 299 | } 300 | 301 | /** 302 | * Callback for 'cellValueChange', updating the cell data 303 | * @param {Array: [number: row, number: cell]} cell [Selected Cell] 304 | * @param {object} newValue [Value to set] 305 | */ 306 | 307 | }, { 308 | key: 'handleCellValueChange', 309 | value: function handleCellValueChange(cell, newValue) { 310 | var data = this.state.data, 311 | row = cell[0], 312 | column = cell[1], 313 | oldValue = data.rows[row][column]; 314 | 315 | _dispatcher2.default.publish('cellValueChanged', [cell, newValue, oldValue], this.state.id); 316 | 317 | data.rows[row][column] = newValue; 318 | this.setState({ 319 | data: data 320 | }); 321 | 322 | _dispatcher2.default.publish('dataChanged', data, this.state.id); 323 | } 324 | 325 | /** 326 | * Callback for 'doubleClickonCell', enabling 'edit' mode 327 | */ 328 | 329 | }, { 330 | key: 'handleDoubleClickOnCell', 331 | value: function handleDoubleClickOnCell() { 332 | this.setState({ 333 | editing: true 334 | }); 335 | } 336 | 337 | /** 338 | * Callback for 'cellBlur' 339 | */ 340 | 341 | }, { 342 | key: 'handleCellBlur', 343 | value: function handleCellBlur(cell) { 344 | if (this.state.editing) { 345 | _dispatcher2.default.publish('editStopped', this.state.selectedElement); 346 | } 347 | 348 | this.setState({ 349 | editing: false, 350 | lastBlurred: cell 351 | }); 352 | } 353 | }]); 354 | 355 | return SpreadsheetComponent; 356 | }(_react.Component); 357 | 358 | module.exports = SpreadsheetComponent; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-spreadsheet-component", 3 | "description": "A Spreadsheet Component for ReactJS", 4 | "version": "1.0.2", 5 | "license": "MIT", 6 | "repository": "felixrieseberg/React-Spreadsheet-Component", 7 | "maintainers": [ 8 | { 9 | "name": "Felix Rieseberg", 10 | "email": "felix@felixrieseberg.com", 11 | "web": "http://www.felixrieseberg.com" 12 | } 13 | ], 14 | "dependencies": { 15 | "jquery": "^2.1", 16 | "mousetrap": "^1.5.3" 17 | }, 18 | "peerDependencies": { 19 | "react": "^15.6.0" 20 | }, 21 | "devDependencies": { 22 | "babel-jest": "^21.2.0", 23 | "babel-preset-es2015": "^6.24.1", 24 | "babel-preset-react": "^6.24.1", 25 | "babelify": "^7.3.0", 26 | "body-parser": "^1.12.4", 27 | "browserify": "^12.0.1", 28 | "browserify-shim": "^3.8.11", 29 | "del": "^2.0.2", 30 | "express": "^4.12.3", 31 | "gulp": "^3.8.11", 32 | "gulp-babel": "^7.0.0", 33 | "gulp-connect": "^4.0.0", 34 | "gulp-header": "^1.2.2", 35 | "gulp-jshint": "^1.9.4", 36 | "gulp-plumber": "^1.0.0", 37 | "gulp-react": "^3.0.1", 38 | "gulp-rename": "^1.2.0", 39 | "gulp-streamify": "1.0.2", 40 | "gulp-uglify": "^1.1.0", 41 | "gulp-util": "^3.0.4", 42 | "jest": "^21.2.1", 43 | "jshint-stylish": "^2.0.1", 44 | "minimatch": "^3.0.4", 45 | "multer": "^1.1.0", 46 | "react": "^15.6.0", 47 | "react-dom": "^15.6.0", 48 | "react-test-renderer": "^15.6.0", 49 | "tape": "^4.2.2", 50 | "vinyl-source-stream": "^1.1.0" 51 | }, 52 | "scripts": { 53 | "debug": "gulp --debug", 54 | "dist": "gulp bundle-js --production --release && gulp bundle-js --development --release", 55 | "watch": "gulp", 56 | "test": "jest" 57 | }, 58 | "jest": { 59 | "transform": { 60 | "\\.js*": "/preprocessor.js" 61 | }, 62 | "unmockedModulePathPatterns": [ 63 | "/node_modules/*" 64 | ] 65 | }, 66 | "browserify-shim": { 67 | "react": "global:React", 68 | "react-dom": "global:ReactDOM" 69 | }, 70 | "browserify": { 71 | "transform": [ 72 | "browserify-shim" 73 | ] 74 | }, 75 | "main": "./lib/spreadsheet.js", 76 | "standalone": "React-Spreadsheet" 77 | } 78 | -------------------------------------------------------------------------------- /preprocessor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const babel = require('babel-core'); 4 | const jestPreset = require('babel-preset-jest'); 5 | 6 | module.exports = { 7 | process(src, filename) { 8 | if (babel.util.canCompile(filename)) { 9 | return babel.transform(src, { 10 | filename, 11 | presets: [jestPreset], 12 | retainLines: true, 13 | }).code; 14 | } 15 | return src; 16 | }, 17 | }; -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## Spreadsheet Component for React 2 | [![Build Status](https://travis-ci.org/felixrieseberg/React-Spreadsheet-Component.svg?branch=master)](https://travis-ci.org/felixrieseberg/React-Spreadsheet-Component) [![Dependency Status](https://david-dm.org/felixrieseberg/react-spreadsheet-component.svg)](https://david-dm.org/felixrieseberg/react-spreadsheet-component) [![npm version](https://badge.fury.io/js/react-spreadsheet-component.svg)](https://badge.fury.io/js/react-spreadsheet-component) ![Downloads](https://img.shields.io/npm/dm/react-spreadsheet-component.svg) 3 | 4 | > :warning: Maintainers wanted! I haven't been able to keep this component up to date and would gladly hand the keys over to someone who is. 5 | 6 | This is a spreadsheet component built in Facebook's ReactJS. [You can see a demo here](http://felixrieseberg.github.io/React-Spreadsheet-Component/). 7 | 8 | ![Screenshot](https://raw.githubusercontent.com/felixrieseberg/React-Spreadsheet-Component/master/example/.reactspreadsheet.gif) 9 | ![Screenshot](https://raw.githubusercontent.com/felixrieseberg/React-Spreadsheet-Component/master/example/.reactspreadsheet2.gif) 10 | 11 | ## Usage 12 | The component is initialized with a configuration object. If desired, initial data for the spreadsheet can be passed in as an array of rows. In addition, you can pass in a second array filled with class names for each cell, allowing you to style each cell differently. 13 | 14 | ```js 15 | var SpreadsheetComponent = require('react-spreadsheet-component'); 16 | React.render(, document.getElementsByTagName('body')); 17 | ``` 18 | 19 | ##### Configuration Object 20 | ```js 21 | var config = { 22 | // Initial number of row 23 | rows: 5, 24 | // Initial number of columns 25 | columns: 8, 26 | // True if the first column in each row is a header (th) 27 | hasHeadColumn: true, 28 | // True if the data for the first column is just a string. 29 | // Set to false if you want to pass custom DOM elements. 30 | isHeadColumnString: true, 31 | // True if the first row is a header (th) 32 | hasHeadRow: true, 33 | // True if the data for the cells in the first row contains strings. 34 | // Set to false if you want to pass custom DOM elements. 35 | isHeadRowString: true, 36 | // True if the user can add rows (by navigating down from the last row) 37 | canAddRow: true, 38 | // True if the user can add columns (by navigating right from the last column) 39 | canAddColumn: true, 40 | // Override the display value for an empty cell 41 | emptyValueSymbol: '-', 42 | // Fills the first column with index numbers (1...n) and the first row with index letters (A...ZZZ) 43 | hasLetterNumberHeads: true 44 | }; 45 | ``` 46 | 47 | ##### Initial Data Object 48 | The initial data object contains an array `rows`, which itself contains an array for every single row. Each index in the array represents a cell. It is used by the component to pre-populate the cells with data. Be aware that user input is not written to this object, as it is merely used in initialization to populate the state. If you want to capture user input, read below. 49 | 50 | ```js 51 | var data = { 52 | rows: [ 53 | ['Key', 'AAA', 'BBB', 'CCC', 'DDD', 'EEE', 'FFF', 'GGG'], 54 | ['COM', '0,0', '0,1', '0,2', '0,3', '0,4', '0,5', '0,6'], 55 | ['DIV', '1,0', '1,1', '1,2', '1,3', '1,4', '1,5', '1,6'], 56 | ['DEV', '2,0', '2,1', '2,2', '2,3', '2,4', '2,5', '2,6'], 57 | ['ACC', '3,0', '3,1', '3,2', '3,3', '3,4', '3,5', '3,6'] 58 | ] 59 | }; 60 | ``` 61 | 62 | ##### Cell Classes Object 63 | You can assign custom classes to individual cells. Follow the same schema as for the initial data object. 64 | 65 | ```js 66 | var classes = { 67 | rows: [ 68 | ['', 'specialHead', '', '', '', '', '', ''], 69 | ['', '', '', '', '', '', '', ''], 70 | ['', 'error', '', '', '', '', '', ''], 71 | ['', 'error changed', '', '', '', '', '', ''], 72 | ['', '', '', '', '', '', '', ''] 73 | ] 74 | }; 75 | ``` 76 | 77 | ## Data Lifecycle 78 | User input is not written to the `initialData` object, as it is merely used in initialization to populate the state. If you want to capture user input, subscribe callbacks to the `cellValueChanged` and `dataChanged` events on the dispatcher. 79 | 80 | The last parameter is the `spreadsheetId` of the spreadsheet you want to subscribe to. 81 | 82 | ##### Get the full data state after each change 83 | ```js 84 | var Dispatcher = require('./src/dispatcher'); 85 | 86 | Dispatcher.subscribe('dataChanged', function (data) { 87 | // data: The entire new data state 88 | }, "spreadsheet-1") 89 | ``` 90 | ##### Get the data change before state change 91 | ```js 92 | var Dispatcher = require('./src/dispatcher'); 93 | 94 | Dispatcher.subscribe('cellValueChanged', function (cell, newValue, oldValue) { 95 | // cell: An array indicating the cell position by row/column, ie: [1,1] 96 | // newValue: The new value for that cell 97 | }, "spreadsheet-1") 98 | ``` 99 | 100 | ### Other Dispatcher Events 101 | The dispatcher offers some other events you can subscribe to: 102 | * `headCellClicked` A head cell was clicked (returns a cell array `[row, column]`) 103 | * `cellSelected` A cell was selected (returns a cell array `[row, column]`) 104 | * `cellBlur` A cell was blurred (returns returns a cell array `[row, column]`) 105 | * `editStarted` The user started editing (returns a cell array `[row, column]`) 106 | * `editStopped` The user stopped editing (returns a cell array `[row, column]`) 107 | * `newRow` The user created a new row (returns the row index) 108 | * `newColumn` The user created a new column (returns the row index) 109 | 110 | ## Running the Example 111 | Clone the repository from GitHub and open the created folder: 112 | 113 | ```bash 114 | git clone https://github.com/felixrieseberg/React-Spreadsheet-Component.git 115 | cd React-Spreadsheet-Component 116 | ``` 117 | 118 | Install npm packages and compile JSX 119 | ```bash 120 | npm install 121 | gulp 122 | ``` 123 | 124 | If you are using Windows, run the following commands instead: 125 | ```bash 126 | npm install --msvs_version=2013 127 | gulp 128 | ``` 129 | 130 | Open the example in example/index.html. 131 | 132 | ## License 133 | (C) Copyright 2015 Microsoft Corporation and Felix Rieseberg. Licensed as MIT, please see `LICENSE` for details. 134 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/cell-test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Cell Renders a cell 1`] = ` 4 | 5 | 6 | 7 | 21 | 22 | 23 |
10 |
13 | 17 | test 18 | 19 |
20 |
24 | `; 25 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/row-test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Row Renders a row 1`] = ` 4 | 5 | 6 | 20 | 21 |
9 |
12 | 16 | 17 | 18 |
19 |
22 | `; 23 | -------------------------------------------------------------------------------- /src/__tests__/cell-test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | jest.dontMock('../cell'); 4 | 5 | import React from 'react'; 6 | import renderer from 'react-test-renderer'; 7 | 8 | import CellComponent from '../cell'; 9 | 10 | const testVars = { 11 | key: 'row_0000_cell_1', 12 | uid: [0, 0], 13 | val: 'test', 14 | spreadsheetId: '1', 15 | selected: false, 16 | editing: false 17 | }; 18 | 19 | describe('Cell', () => { 20 | it('Renders a cell', () => { 21 | const cell = renderer.create( 22 | 23 | 24 | 25 | 33 | 34 | 35 |
36 | ).toJSON(); 37 | expect(cell).toMatchSnapshot() 38 | }); 39 | }); -------------------------------------------------------------------------------- /src/__tests__/helpers-test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | jest.dontMock('../helpers'); 4 | 5 | const Helpers = require('../helpers'); 6 | 7 | describe('Helpers', () => { 8 | it('Correctly finds the first element in an array', () => { 9 | let arr = [{myProp: 1}, {myProp: 2}, {myProp: 3, xProp: 1}, {myProp: 4}, {myProp: 3, xProp: 2}]; 10 | 11 | let firstFound = Helpers.firstInArray(arr, function (element) { 12 | return (element.myProp === 3); 13 | }); 14 | 15 | expect(firstFound.myProp).toBe(3); 16 | expect(firstFound.xProp).toBe(1); 17 | }); 18 | 19 | it('Correctly finds the first element in an array', () => { 20 | let arr = [{nodeName: 'x'}, {nodeName: 'TD', myProp: 1}, {nodeName: 'TD', myProp: 2}]; 21 | 22 | let firstFound = Helpers.firstTDinArray(arr); 23 | 24 | expect(firstFound.nodeName).toBe('TD'); 25 | expect(firstFound.myProp).toBe(1); 26 | }); 27 | 28 | it('Correctly identifies two cells as equal', () => { 29 | let cell1 = ['prop', 'propTwo']; 30 | let cell2 = ['prop', 'propTwo']; 31 | 32 | let cellsEqual = Helpers.equalCells(cell1, cell2); 33 | 34 | expect(cellsEqual).toBe(true); 35 | }); 36 | 37 | it('Correctly identifies two cells as unequal', () => { 38 | let cell1 = ['prop', 'propTwo']; 39 | let cell2 = ['prop', 'propThree']; 40 | 41 | let cellsEqual = Helpers.equalCells(cell1, cell2); 42 | 43 | expect(cellsEqual).toBe(false); 44 | }); 45 | 46 | it('Correctly counts with letters', () => { 47 | expect(Helpers.countWithLetters(1)).toBe('A'); 48 | expect(Helpers.countWithLetters(2)).toBe('B'); 49 | expect(Helpers.countWithLetters(26)).toBe('Z'); 50 | expect(Helpers.countWithLetters(27)).toBe('AA'); 51 | expect(Helpers.countWithLetters(28)).toBe('AB'); 52 | }); 53 | 54 | it('Correctly makes a spreadsheet id', () => { 55 | expect(Helpers.makeSpreadsheetId().length).toBe(5); 56 | expect(Helpers.makeSpreadsheetId().length).toBe(5); 57 | expect(Helpers.makeSpreadsheetId().length).toBe(5); 58 | expect(Helpers.makeSpreadsheetId().length).toBe(5); 59 | }) 60 | }); -------------------------------------------------------------------------------- /src/__tests__/row-test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | jest.dontMock('../row'); 4 | 5 | import React from 'react'; 6 | import renderer from 'react-test-renderer'; 7 | 8 | import RowComponent from '../cell'; 9 | 10 | const testVars = { 11 | cells: [], 12 | cellClasses: [], 13 | uid: [0], 14 | key: 'testkey', 15 | spreadsheetId: '0', 16 | className: 'cellComponent' 17 | }; 18 | 19 | describe('Row', () => { 20 | it('Renders a row', () => { 21 | const row = renderer.create( 22 | 23 | 24 | 32 | 33 |
34 | ).toJSON(); 35 | expect(row).toMatchSnapshot(); 36 | }); 37 | }); -------------------------------------------------------------------------------- /src/__tests__/spreadsheet-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | jest.dontMock('../spreadsheet'); 4 | 5 | import React from 'react'; 6 | import renderer from 'react-test-renderer'; 7 | 8 | import SpreadsheetComponent from '../spreadsheet'; 9 | 10 | const testVars = { 11 | initialData: { 12 | rows: [ 13 | ['', '', '', '', '', '', '', ''], 14 | ['', 1, 2, 3, 4, 5, 6, 7], 15 | ['', 1, '', 3, 4, 5, 6, 7], 16 | ['', 1, 2, 3, 4, 5, 6, 7], 17 | ['', 1, 2, 3, 4, 5, 6, 7] 18 | ] 19 | }, 20 | config: { 21 | rows: 5, 22 | columns: 8, 23 | hasHeadColumn: true, 24 | isHeadColumnString: true, 25 | hasHeadRow: true, 26 | isHeadRowString: true, 27 | canAddRow: true, 28 | canAddColumn: true, 29 | emptyValueSymbol: '-', 30 | hasLetterNumberHeads: true 31 | } 32 | }; 33 | 34 | describe('Spreadsheet', () => { 35 | it('Renders a spreadsheet', () => { 36 | let spreadsheet = renderer.create( 37 | 42 | ).toJSON(); 43 | expect(spreadsheet).toMatchSnapshot; 44 | }); 45 | }); -------------------------------------------------------------------------------- /src/cell.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import Dispatcher from './dispatcher'; 4 | import Helpers from './helpers'; 5 | 6 | class CellComponent extends Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | editing: this.props.editing, 11 | changedValue: this.props.value 12 | } 13 | } 14 | 15 | /** 16 | * React "render" method, rendering the individual cell 17 | */ 18 | render() { 19 | var props = this.props, 20 | selected = (props.selected) ? 'selected' : '', 21 | ref = 'input_' + props.uid.join('_'), 22 | config = props.config || { emptyValueSymbol: ''}, 23 | displayValue = (props.value === '' || !props.value) ? config.emptyValueSymbol : props.value, 24 | cellClasses = (props.cellClasses && props.cellClasses.length > 0) ? props.cellClasses + ' ' + selected : selected, 25 | cellContent; 26 | 27 | // Check if header - if yes, render it 28 | var header = this.renderHeader(); 29 | if (header) { 30 | return header; 31 | } 32 | 33 | // If not a header, check for editing and return 34 | if (props.selected && props.editing) { 35 | cellContent = ( 36 | 41 | ) 42 | } 43 | 44 | return ( 45 | 46 |
47 | {cellContent} 48 | 49 | {displayValue} 50 | 51 |
52 | 53 | ); 54 | } 55 | 56 | /** 57 | * React "componentDidUpdate" method, ensuring correct input focus 58 | * @param {React previous properties} prevProps 59 | * @param {React previous state} prevState 60 | */ 61 | componentDidUpdate(prevProps, prevState) { 62 | if (this.props.editing && this.props.selected) { 63 | var node = this.refs['input_' + this.props.uid.join('_')]; 64 | node.focus(); 65 | } 66 | 67 | if (prevProps.selected && prevProps.editing && this.state.changedValue !== this.props.value) { 68 | this.props.onCellValueChange(this.props.uid, this.state.changedValue); 69 | } 70 | } 71 | 72 | /** 73 | * Click handler for individual cell, ensuring navigation and selection 74 | * @param {event} e 75 | */ 76 | handleClick(e) { 77 | var cellElement = this.refs[this.props.uid.join('_')]; 78 | this.props.handleSelectCell(this.props.uid, cellElement); 79 | } 80 | 81 | /** 82 | * Click handler for individual cell if the cell is a header cell 83 | * @param {event} e 84 | */ 85 | handleHeadClick(e) { 86 | var cellElement = this.refs[this.props.uid.join('_')]; 87 | Dispatcher.publish('headCellClicked', cellElement, this.props.spreadsheetId); 88 | } 89 | 90 | /** 91 | * Double click handler for individual cell, ensuring navigation and selection 92 | * @param {event} e 93 | */ 94 | handleDoubleClick(e) { 95 | e.preventDefault(); 96 | this.props.handleDoubleClickOnCell(this.props.uid); 97 | } 98 | 99 | /** 100 | * Blur handler for individual cell 101 | * @param {event} e 102 | */ 103 | handleBlur(e) { 104 | var newValue = this.refs['input_' + this.props.uid.join('_')].value; 105 | 106 | this.props.onCellValueChange(this.props.uid, newValue, e); 107 | this.props.handleCellBlur(this.props.uid); 108 | Dispatcher.publish('cellBlurred', this.props.uid, this.props.spreadsheetId); 109 | } 110 | 111 | /** 112 | * Change handler for an individual cell, propagating the value change 113 | * @param {event} e 114 | */ 115 | handleChange(e) { 116 | var newValue = this.refs['input_' + this.props.uid.join('_')].value; 117 | this.setState({changedValue: newValue}); 118 | } 119 | 120 | /** 121 | * Checks if a header exists - if it does, it returns a header object 122 | * @return {false|react} [Either false if it's not a header cell, a react object if it is] 123 | */ 124 | renderHeader() { 125 | var props = this.props, 126 | selected = (props.selected) ? 'selected' : '', 127 | uid = props.uid, 128 | config = props.config || { emptyValueSymbol: ''}, 129 | displayValue = (props.value === '' || !props.value) ? config.emptyValueSymbol : props.value, 130 | cellClasses = (props.cellClasses && props.cellClasses.length > 0) ? this.props.cellClasses + ' ' + selected : selected; 131 | 132 | // Cases 133 | var headRow = (uid[0] === 0), 134 | headColumn = (uid[1] === 0), 135 | headRowAndEnabled = (config.hasHeadRow && uid[0] === 0), 136 | headColumnAndEnabled = (config.hasHeadColumn && uid[1] === 0) 137 | 138 | // Head Row enabled, cell is in head row 139 | // Head Column enabled, cell is in head column 140 | if (headRowAndEnabled || headColumnAndEnabled) { 141 | if (headColumn && config.hasLetterNumberHeads) { 142 | displayValue = uid[0]; 143 | } else if (headRow && config.hasLetterNumberHeads) { 144 | displayValue = Helpers.countWithLetters(uid[1]); 145 | } 146 | 147 | if ((config.isHeadRowString && headRow) || (config.isHeadColumnString && headColumn)) { 148 | return ( 149 | 150 |
151 | 152 | {displayValue} 153 | 154 |
155 | 156 | ); 157 | } else { 158 | return ( 159 | 160 | {displayValue} 161 | 162 | ); 163 | } 164 | } else { 165 | return false; 166 | } 167 | } 168 | } 169 | 170 | module.exports = CellComponent; 171 | -------------------------------------------------------------------------------- /src/dispatcher.js: -------------------------------------------------------------------------------- 1 | var Mousetrap = require('mousetrap'); 2 | var $ = require('jquery'); 3 | 4 | var dispatcher = { 5 | // Event Pub/Sub System 6 | // 7 | // Topics used: 8 | // [headCellClicked] - A head cell was clicked 9 | // @return {array} [row, column] 10 | // [cellSelected] - A cell was selected 11 | // @return {array} [row, column] 12 | // [cellBlur] - A cell was blurred 13 | // @return {array} [row, column] 14 | // [cellValueChanged] - A cell value changed. 15 | // @return {cell, newValue} Origin cell, new value entered 16 | // [dataChanged] - Data changed 17 | // @return {data} New data 18 | // [editStarted] - The user started editing 19 | // @return {cell} Origin cell 20 | // [editStopped] - The user stopped editing 21 | // @return {cell} Origin cell 22 | // [rowCreated] - The user created a row 23 | // @return {number} Row index 24 | // [columnCreated] - The user created a column 25 | // @return {number} Column index 26 | topics: {}, 27 | 28 | /** 29 | * Subscribe to an event 30 | * @param {string} topic [The topic subscribing to] 31 | * @param {function} listener [The callback for published events] 32 | * @param {string} spreadsheetId [The reactId (data-spreadsheetId) of the origin element] 33 | */ 34 | subscribe: function(topic, listener, spreadsheetId) { 35 | if (!this.topics[spreadsheetId]) { 36 | this.topics[spreadsheetId] = []; 37 | } 38 | 39 | if (!this.topics[spreadsheetId][topic]) { 40 | this.topics[spreadsheetId][topic] = []; 41 | } 42 | 43 | this.topics[spreadsheetId][topic].push(listener); 44 | }, 45 | 46 | /** 47 | * Publish to an event channel 48 | * @param {string} topic [The topic publishing to] 49 | * @param {object} data [An object passed to the subscribed callbacks] 50 | * @param {string} spreadsheetId [The reactId (data-spreadsheetId) of the origin element] 51 | */ 52 | publish: function(topic, data, spreadsheetId) { 53 | // return if the topic doesn't exist, or there are no listeners 54 | if (!this.topics[spreadsheetId] || !this.topics[spreadsheetId][topic] || this.topics[spreadsheetId][topic].length < 1) { 55 | return 56 | } 57 | 58 | this.topics[spreadsheetId][topic].forEach(function(listener) { 59 | listener(data || {}); 60 | }); 61 | }, 62 | 63 | keyboardShortcuts: [ 64 | // Name, Keys, Events 65 | ['down', 'down', ['keyup']], 66 | ['up', 'up', ['keyup']], 67 | ['left', 'left', ['keyup']], 68 | ['right', 'right', ['keyup']], 69 | ['tab', 'tab', ['keyup', 'keydown']], 70 | ['enter', 'enter', ['keyup']], 71 | ['esc', 'esc', ['keyup']], 72 | ['remove', ['backspace', 'delete'], ['keyup', 'keydown']], 73 | ['letter', ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'x', 'w', 'y', 'z', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '=', '.', ',', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'X', 'W', 'Y', 'Z'], ['keyup', 'keydown']] 74 | ], 75 | 76 | /** 77 | * Initializes the keyboard bindings 78 | * @param {object} domNode [The DOM node of the element that should be bound] 79 | * @param {string} spreadsheetId [The id of the spreadsheet element] 80 | */ 81 | setupKeyboardShortcuts: function (domNode, spreadsheetId) { 82 | var self = this; 83 | this.keyboardShortcuts.map(function (shortcut) { 84 | var shortcutName = shortcut[0], 85 | shortcutKey = shortcut[1], 86 | events = shortcut[2]; 87 | 88 | events.map(event => { 89 | Mousetrap(domNode).bind(shortcutKey, function (e) { 90 | self.publish(shortcutName + '_' + event, e, spreadsheetId); 91 | }, event); 92 | }) 93 | }); 94 | 95 | // Avoid scroll 96 | window.addEventListener('keydown', function(e) { 97 | // space and arrow keys 98 | if ([32, 37, 38, 39, 40].indexOf(e.keyCode) > -1 && $(document.activeElement)[0].tagName !== 'INPUT') { 99 | if (e.preventDefault) { 100 | e.preventDefault(); 101 | } else { 102 | // Oh, old IE, you 💩 103 | e.returnValue = false; 104 | } 105 | } 106 | }, false); 107 | } 108 | }; 109 | 110 | module.exports = dispatcher; -------------------------------------------------------------------------------- /src/helpers.js: -------------------------------------------------------------------------------- 1 | var Helpers = { 2 | /** 3 | * Find the first element in an array matching a boolean 4 | * @param {[array]} arr [Array to test] 5 | * @param {[function]} test [Test Function] 6 | * @param {[type]} context [Context] 7 | * @return {[object]} [Found element] 8 | */ 9 | firstInArray: function (arr, test, context) { 10 | var result = null; 11 | 12 | arr.some(function(el, i) { 13 | return test.call(context, el, i, arr) ? ((result = el), true) : false; 14 | }); 15 | 16 | return result; 17 | }, 18 | 19 | /** 20 | * Find the first TD in a path array 21 | * @param {[array]} arr [Path array containing elements] 22 | * @return {[object]} [Found element] 23 | */ 24 | firstTDinArray: function (arr) { 25 | var cell = Helpers.firstInArray(arr, function (element) { 26 | if (element.nodeName && element.nodeName === 'TD') { 27 | return true; 28 | } else { 29 | return false; 30 | } 31 | }); 32 | 33 | return cell; 34 | }, 35 | 36 | /** 37 | * Check if two cell objects reference the same cell 38 | * @param {[array]} cell1 [First cell] 39 | * @param {[array]} cell2 [Second cell] 40 | * @return {[boolean]} [Boolean indicating if the cells are equal] 41 | */ 42 | equalCells: function (cell1, cell2) { 43 | if (!cell1 || !cell2 || cell1.length !== cell2.length) { 44 | return false; 45 | } 46 | 47 | if (cell1[0] === cell2[0] && cell1[1] === cell2[1]) { 48 | return true; 49 | } else { 50 | return false; 51 | } 52 | }, 53 | 54 | /** 55 | * Counts in letters (A, B, C...Z, AA); 56 | * @return {[string]} [Letter] 57 | */ 58 | countWithLetters: function (num) { 59 | var mod = num % 26, 60 | pow = num / 26 | 0, 61 | out = mod ? String.fromCharCode(64 + mod) : (--pow, 'Z'); 62 | return pow ? this.countWithLetters(pow) + out : out; 63 | }, 64 | 65 | /** 66 | * Creates a random 5-character id 67 | * @return {string} [Somewhat random id] 68 | */ 69 | makeSpreadsheetId: function() 70 | { 71 | var text = '', 72 | possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 73 | 74 | for (var i = 0; i < 5; i = i + 1) { 75 | text += possible.charAt(Math.floor(Math.random() * possible.length)); 76 | } 77 | 78 | return text; 79 | } 80 | } 81 | 82 | module.exports = Helpers; -------------------------------------------------------------------------------- /src/row.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import CellComponent from './cell'; 3 | import Helpers from './helpers'; 4 | 5 | class RowComponent extends Component { 6 | /** 7 | * React Render method 8 | * @return {[JSX]} [JSX to render] 9 | */ 10 | render() { 11 | var config = this.props.config, 12 | cells = this.props.cells, 13 | columns = [], 14 | key, uid, selected, cellClasses, i; 15 | 16 | if (!config.columns || cells.length === 0) { 17 | return console.error('Table can\'t be initialized without set number of columsn and no data!'); 18 | } 19 | 20 | for (i = 0; i < cells.length; i = i + 1) { 21 | // If a cell is selected, check if it's this one 22 | selected = Helpers.equalCells(this.props.selected, [this.props.uid, i]); 23 | cellClasses = (this.props.cellClasses && this.props.cellClasses[i]) ? this.props.cellClasses[i] : ''; 24 | 25 | key = 'row_' + this.props.uid + '_cell_' + i; 26 | uid = [this.props.uid, i]; 27 | columns.push( 39 | ); 40 | } 41 | 42 | return {columns}; 43 | } 44 | } 45 | 46 | module.exports = RowComponent; -------------------------------------------------------------------------------- /src/spreadsheet.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import RowComponent from './row'; 4 | import Dispatcher from './dispatcher'; 5 | import Helpers from './helpers'; 6 | 7 | var $ = require('jquery'); 8 | 9 | class SpreadsheetComponent extends Component { 10 | 11 | constructor(props) { 12 | super(props); 13 | 14 | var initialData = this.props.initialData || {}; 15 | 16 | if (!initialData.rows) { 17 | initialData.rows = []; 18 | 19 | for (var i = 0; i < this.props.config.rows; i = i + 1) { 20 | initialData.rows[i] = []; 21 | for (var ci = 0; ci < this.props.config.columns; ci = ci + 1) { 22 | initialData.rows[i][ci] = ''; 23 | } 24 | } 25 | } 26 | 27 | this.state = { 28 | data: initialData, 29 | selected: null, 30 | lastBlurred: null, 31 | selectedElement: null, 32 | editing: false, 33 | id: this.props.spreadsheetId || Helpers.makeSpreadsheetId() 34 | }; 35 | } 36 | 37 | /** 38 | * React 'componentDidMount' method 39 | */ 40 | componentDidMount() { 41 | this.bindKeyboard(); 42 | 43 | $('body').on('focus', 'input', function (e) { 44 | $(this) 45 | .one('mouseup', function () { 46 | $(this).select(); 47 | return false; 48 | }) 49 | .select(); 50 | }); 51 | } 52 | 53 | /** 54 | * React Render method 55 | * @return {[JSX]} [JSX to render] 56 | */ 57 | render() { 58 | var data = this.state.data, 59 | config = this.props.config, 60 | _cellClasses = this.props.cellClasses, 61 | rows = [], key, i, cellClasses; 62 | 63 | // Sanity checks 64 | if (!data.rows && !config.rows) { 65 | return console.error('Table Component: Number of colums not defined in both data and config!'); 66 | } 67 | 68 | // Create Rows 69 | for (i = 0; i < data.rows.length; i = i + 1) { 70 | key = 'row_' + i; 71 | cellClasses = (_cellClasses && _cellClasses.rows && _cellClasses.rows[i]) ? _cellClasses.rows[i] : null; 72 | 73 | rows.push(); 86 | } 87 | 88 | return ( 89 | 90 | 91 | {rows} 92 | 93 |
94 | ); 95 | } 96 | 97 | /** 98 | * Binds the various keyboard events dispatched to table functions 99 | */ 100 | bindKeyboard() { 101 | Dispatcher.setupKeyboardShortcuts($(this.refs["react-spreadsheet-"+this.state.id])[0], this.state.id); 102 | 103 | Dispatcher.subscribe('up_keyup', data => { 104 | this.navigateTable('up', data); 105 | }, this.state.id); 106 | Dispatcher.subscribe('down_keyup', data => { 107 | this.navigateTable('down', data); 108 | }, this.state.id); 109 | Dispatcher.subscribe('left_keyup', data => { 110 | this.navigateTable('left', data); 111 | }, this.state.id); 112 | Dispatcher.subscribe('right_keyup', data => { 113 | this.navigateTable('right', data); 114 | }, this.state.id); 115 | Dispatcher.subscribe('tab_keyup', data => { 116 | this.navigateTable('right', data, null, true); 117 | }, this.state.id); 118 | 119 | // Prevent brower's from jumping to URL bar 120 | Dispatcher.subscribe('tab_keydown', data => { 121 | if ($(document.activeElement) && $(document.activeElement)[0].tagName === 'INPUT') { 122 | if (data.preventDefault) { 123 | data.preventDefault(); 124 | } else { 125 | // Oh, old IE, you 💩 126 | data.returnValue = false; 127 | } 128 | } 129 | }, this.state.id); 130 | 131 | Dispatcher.subscribe('remove_keydown', data => { 132 | if (!$(data.target).is('input, textarea')) { 133 | if (data.preventDefault) { 134 | data.preventDefault(); 135 | } else { 136 | // Oh, old IE, you 💩 137 | data.returnValue = false; 138 | } 139 | } 140 | }, this.state.id); 141 | 142 | Dispatcher.subscribe('enter_keyup', () => { 143 | if (this.state.selectedElement) { 144 | this.setState({editing: !this.state.editing}); 145 | } 146 | $(this.refs["react-spreadsheet-"+this.state.id]).first().focus(); 147 | }, this.state.id); 148 | 149 | // Go into edit mode when the user starts typing on a field 150 | Dispatcher.subscribe('letter_keydown', () => { 151 | if (!this.state.editing && this.state.selectedElement) { 152 | Dispatcher.publish('editStarted', this.state.selectedElement, this.state.id); 153 | this.setState({editing: true}); 154 | } 155 | }, this.state.id); 156 | 157 | // Delete on backspace and delete 158 | Dispatcher.subscribe('remove_keyup', () => { 159 | if (this.state.selected && !Helpers.equalCells(this.state.selected, this.state.lastBlurred)) { 160 | this.handleCellValueChange(this.state.selected, ''); 161 | } 162 | }, this.state.id); 163 | } 164 | 165 | /** 166 | * Navigates the table and moves selection 167 | * @param {string} direction [Direction ('up' || 'down' || 'left' || 'right')] 168 | * @param {Array: [number: row, number: cell]} originCell [Origin Cell] 169 | * @param {boolean} inEdit [Currently editing] 170 | */ 171 | navigateTable(direction, data, originCell, inEdit) { 172 | // Only traverse the table if the user isn't editing a cell, 173 | // unless override is given 174 | if (!inEdit && this.state.editing) { 175 | return false; 176 | } 177 | 178 | // Use the curently active cell if one isn't passed 179 | if (!originCell) { 180 | originCell = this.state.selectedElement; 181 | } 182 | 183 | // Prevent default 184 | if (data.preventDefault) { 185 | data.preventDefault(); 186 | } else { 187 | // Oh, old IE, you 💩 188 | data.returnValue = false; 189 | } 190 | 191 | var $origin = $(originCell), 192 | cellIndex = $origin.index(), 193 | target; 194 | 195 | if (direction === 'up') { 196 | target = $origin.closest('tr').prev().children().eq(cellIndex).find('span'); 197 | } else if (direction === 'down') { 198 | target = $origin.closest('tr').next().children().eq(cellIndex).find('span'); 199 | } else if (direction === 'left') { 200 | target = $origin.closest('td').prev().find('span'); 201 | } else if (direction === 'right') { 202 | target = $origin.closest('td').next().find('span'); 203 | } 204 | 205 | if (target.length > 0) { 206 | target.click(); 207 | } else { 208 | this.extendTable(direction, originCell); 209 | } 210 | } 211 | 212 | /** 213 | * Extends the table with an additional row/column, if permitted by config 214 | * @param {string} direction [Direction ('up' || 'down' || 'left' || 'right')] 215 | */ 216 | extendTable(direction) { 217 | var config = this.props.config, 218 | data = this.state.data, 219 | newRow, i; 220 | 221 | if (direction === 'down' && config.canAddRow) { 222 | newRow = []; 223 | 224 | for (i = 0; i < this.state.data.rows[0].length; i = i + 1) { 225 | newRow[i] = ''; 226 | } 227 | 228 | data.rows.push(newRow); 229 | Dispatcher.publish('rowCreated', data.rows.length, this.state.id); 230 | return this.setState({data: data}); 231 | } 232 | 233 | if (direction === 'right' && config.canAddColumn) { 234 | for (i = 0; i < data.rows.length; i = i + 1) { 235 | data.rows[i].push(''); 236 | } 237 | 238 | Dispatcher.publish('columnCreated', data.rows[0].length, this.state.id); 239 | return this.setState({data: data}); 240 | } 241 | 242 | } 243 | 244 | /** 245 | * Callback for 'selectCell', updating the selected Cell 246 | * @param {Array: [number: row, number: cell]} cell [Selected Cell] 247 | * @param {object} cellElement [Selected Cell Element] 248 | */ 249 | handleSelectCell(cell, cellElement) { 250 | Dispatcher.publish('cellSelected', cell, this.state.id); 251 | $(this.refs["react-spreadsheet-"+this.state.id]).first().focus(); 252 | 253 | this.setState({ 254 | selected: cell, 255 | selectedElement: cellElement 256 | }); 257 | } 258 | 259 | /** 260 | * Callback for 'cellValueChange', updating the cell data 261 | * @param {Array: [number: row, number: cell]} cell [Selected Cell] 262 | * @param {object} newValue [Value to set] 263 | */ 264 | handleCellValueChange(cell, newValue) { 265 | var data = this.state.data, 266 | row = cell[0], 267 | column = cell[1], 268 | oldValue = data.rows[row][column]; 269 | 270 | Dispatcher.publish('cellValueChanged', [cell, newValue, oldValue], this.state.id); 271 | 272 | data.rows[row][column] = newValue; 273 | this.setState({ 274 | data: data 275 | }); 276 | 277 | Dispatcher.publish('dataChanged', data, this.state.id); 278 | } 279 | 280 | /** 281 | * Callback for 'doubleClickonCell', enabling 'edit' mode 282 | */ 283 | handleDoubleClickOnCell() { 284 | this.setState({ 285 | editing: true 286 | }); 287 | } 288 | 289 | /** 290 | * Callback for 'cellBlur' 291 | */ 292 | handleCellBlur(cell) { 293 | if (this.state.editing) { 294 | Dispatcher.publish('editStopped', this.state.selectedElement); 295 | } 296 | 297 | this.setState({ 298 | editing: false, 299 | lastBlurred: cell 300 | }); 301 | } 302 | } 303 | 304 | module.exports = SpreadsheetComponent; 305 | -------------------------------------------------------------------------------- /styles/creativeworks.css: -------------------------------------------------------------------------------- 1 | body 2 | { 3 | background-color: #2C2C2C; 4 | font-family: Lato, Helvetica, Arial, sans-serif; 5 | color: #fff; 6 | } 7 | 8 | table 9 | { 10 | font-family: Lato, Helvetica, Arial, sans-serif; 11 | font-size: 11px; 12 | 13 | color: #878787; 14 | border-width: 0 0 1px 1px; 15 | border-style: solid; 16 | border-color: #1a1a1a; 17 | border-spacing: 0; 18 | background-color: #262626; 19 | } 20 | 21 | td, th 22 | { 23 | box-sizing: border-box; 24 | position: relative; 25 | display: inline-block; 26 | 27 | height: 35px; 28 | width: 75px; 29 | margin: 0; 30 | padding: 0; 31 | 32 | text-align: center; 33 | 34 | border-width: 1px 1px 0 0; 35 | border-style: solid; 36 | border-color: #1a1a1a; 37 | } 38 | 39 | td div, th div { 40 | height: 100%; 41 | } 42 | 43 | td span 44 | { 45 | display: inline-block; 46 | width: calc(100% - 8px); 47 | height: calc(100% - 8px); 48 | padding: 4px; 49 | line-height: 26px; 50 | } 51 | 52 | td input 53 | { 54 | position: absolute; 55 | z-index: 100; 56 | 57 | font-family: Lato, Helvetica, Arial, sans-serif; 58 | font-size: 11px; 59 | 60 | display: inline-block; 61 | 62 | width: calc(100% - 8px); 63 | height: calc(100% - 8px); 64 | margin: 0; 65 | padding: 4px; 66 | 67 | text-align: center; 68 | 69 | color: #878787; 70 | border: none; 71 | background-color: #202020; 72 | } 73 | 74 | td.selected { 75 | border: 2px solid #7F7F7F; 76 | background-color: #2C2C2C; 77 | } -------------------------------------------------------------------------------- /styles/excel.css: -------------------------------------------------------------------------------- 1 | body 2 | { 3 | font-family: Lato, Helvetica, Arial, sans-serif; 4 | 5 | color: #fff; 6 | background-color: #fff; 7 | } 8 | 9 | table 10 | { 11 | font-family: Calibri, 'Segoe UI', Thonburi, Arial, Verdana, sans-serif; 12 | font-size: 14px; 13 | 14 | border-spacing: 0; 15 | 16 | color: #000; 17 | border-width: 0 0 1px 1px; 18 | border-style: solid; 19 | border-color: #cacaca; 20 | table-layout:fixed; 21 | } 22 | 23 | table:focus { 24 | outline: none; 25 | } 26 | 27 | td, 28 | th 29 | { 30 | position: relative; 31 | 32 | display: inline-block; 33 | 34 | box-sizing: border-box; 35 | width: 80px; 36 | height: 25px; 37 | margin: 0; 38 | padding: 0; 39 | 40 | border-width: 1px 1px 0 0; 41 | border-style: solid; 42 | border-color: #cacaca; 43 | overflow: hidden; 44 | text-overflow: ellipsis; 45 | white-space: nowrap; 46 | } 47 | 48 | th 49 | { 50 | background-color: #f0f0f0; 51 | } 52 | 53 | td div, 54 | th div 55 | { 56 | height: 100%; 57 | } 58 | 59 | td span, th span 60 | { 61 | line-height: 15px; 62 | 63 | display: inline-block; 64 | 65 | width: calc(100% - 8px); 66 | height: calc(100% - 8px); 67 | padding: 4px; 68 | } 69 | 70 | td input 71 | { 72 | font-family: Calibri, 'Segoe UI', Thonburi, Arial, Verdana, sans-serif; 73 | font-size: 14px; 74 | 75 | position: absolute; 76 | z-index: 100; 77 | 78 | display: inline-block; 79 | 80 | width: calc(100% - 8px); 81 | height: calc(100% - 8px); 82 | margin: 0; 83 | padding: 4px; 84 | 85 | color: #000; 86 | border: none; 87 | background-color: #fff; 88 | } 89 | 90 | td.selected 91 | { 92 | border: 2px solid #1e6337; 93 | } 94 | 95 | td.selected span 96 | { 97 | padding: 3px 3px 3px 2px; 98 | } 99 | 100 | input:focus, 101 | select:focus, 102 | textarea:focus, 103 | button:focus 104 | { 105 | outline: none; 106 | } 107 | --------------------------------------------------------------------------------