diff --git a/.vscode/ftp-kr.sync.cache.json b/.vscode/ftp-kr.sync.cache.json
index ef2d8a87..45d2d6fb 100644
--- a/.vscode/ftp-kr.sync.cache.json
+++ b/.vscode/ftp-kr.sync.cache.json
@@ -1031,9 +1031,9 @@
},
"image_log.json": {
"type": "-",
- "size": 977,
+ "size": 128703,
"lmtime": 0,
- "modified": false
+ "modified": true
},
"images.inc.php": {
"type": "-",
@@ -1756,6 +1756,12 @@
"dashgoals": {},
"dashproducts": {},
"dashtrends": {},
+ ".DS_Store": {
+ "type": "-",
+ "size": 22532,
+ "lmtime": 0,
+ "modified": false
+ },
"ets_blog": {},
"ets_multilayerslider": {},
"followup": {},
@@ -1775,306 +1781,6 @@
"lmtime": 0,
"modified": false
},
- "pagesnotfound": {},
- "productcomments": {},
- "ps_accounts": {},
- "ps_banner": {},
- "ps_buybuttonlite": {},
- "ps_categorytree": {},
- "ps_checkout": {},
- "ps_checkpayment": {},
- "ps_contactinfo": {},
- "ps_crossselling": {},
- "ps_currencyselector": {},
- "ps_customeraccountlinks": {},
- "ps_customersignin": {},
- "ps_customtext": {},
- "ps_dataprivacy": {},
- "ps_emailalerts": {},
- "ps_emailsubscription": {},
- "ps_facebook": {},
- "ps_facetedsearch": {
- "_dev": {
- "back": {
- "blocklayered.css": {
- "type": "-",
- "size": 1577,
- "lmtime": 1737287962008,
- "modified": false
- },
- "blocklayered.css.map": {
- "type": "-",
- "size": 4300,
- "lmtime": 1737287962008,
- "modified": false
- },
- "blocklayered.scss": {
- "type": "-",
- "size": 1810,
- "lmtime": 0,
- "modified": false
- },
- "index.js": {
- "type": "-",
- "size": 8041,
- "lmtime": 0,
- "modified": false
- }
- },
- "front": {
- "events.js": {
- "type": "-",
- "size": 1114,
- "lmtime": 0,
- "modified": false
- },
- "facet.css": {
- "type": "-",
- "size": 1529,
- "lmtime": 1737287962008,
- "modified": false
- },
- "facet.css.map": {
- "type": "-",
- "size": 3483,
- "lmtime": 1737287962008,
- "modified": false
- },
- "facet.scss": {
- "type": "-",
- "size": 1398,
- "lmtime": 0,
- "modified": false
- },
- "index.js": {
- "type": "-",
- "size": 911,
- "lmtime": 0,
- "modified": false
- },
- "overlay.css": {
- "type": "-",
- "size": 1560,
- "lmtime": 1737287962008,
- "modified": false
- },
- "overlay.css.map": {
- "type": "-",
- "size": 3763,
- "lmtime": 1737287962008,
- "modified": false
- },
- "overlay.js": {
- "type": "-",
- "size": 1225,
- "lmtime": 0,
- "modified": false
- },
- "overlay.scss": {
- "type": "-",
- "size": 1522,
- "lmtime": 0,
- "modified": false
- },
- "slider.css": {
- "type": "-",
- "size": 1194,
- "lmtime": 1737287962008,
- "modified": false
- },
- "slider.css.map": {
- "type": "-",
- "size": 2760,
- "lmtime": 1737287962008,
- "modified": false
- },
- "slider.js": {
- "type": "-",
- "size": 3816,
- "lmtime": 0,
- "modified": false
- },
- "slider.scss": {
- "type": "-",
- "size": 1179,
- "lmtime": 0,
- "modified": false
- },
- "urlparser.js": {
- "type": "-",
- "size": 1051,
- "lmtime": 0,
- "modified": false
- }
- }
- }
- },
- "ps_faviconnotificationbo": {},
- "ps_featuredproducts": {},
- "psgdpr": {
- "views": {
- "templates": {
- "hook": {
- "displayGDPRConsent.tpl": {
- "type": "-",
- "size": 4075,
- "lmtime": 1737912422958,
- "modified": false
- }
- }
- }
- }
- },
- "ps_imageslider": {},
- "ps_languageselector": {},
- "ps_linklist": {},
- "ps_mainmenu": {},
- "ps_mbo": {},
- "ps_metrics": {},
- "ps_reminder": {},
- "ps_searchbar": {},
- "ps_sharebuttons": {},
- "ps_shoppingcart": {},
- "ps_socialfollow": {},
- "ps_themecusto": {},
- "ps_wirepayment": {},
- "psxmarketingwithgoogle": {},
- "raty": {},
- "referralprogram": {},
- "regenerateimage": {},
- "santandercredit": {
- "CERT": {},
- "config": {},
- "config_pl.xml": {
- "type": "-",
- "size": 507,
- "lmtime": 0,
- "modified": false
- },
- "config.xml": {
- "type": "-",
- "size": 579,
- "lmtime": 0,
- "modified": false
- },
- "controllers": {},
- "doc": {},
- "images": {},
- "index.php": {
- "type": "-",
- "size": 1272,
- "lmtime": 0,
- "modified": false
- },
- "js": {},
- "logo.gif": {
- "type": "-",
- "size": 1217,
- "lmtime": 0,
- "modified": false
- },
- "logo.png": {
- "type": "-",
- "size": 3571,
- "lmtime": 0,
- "modified": false
- },
- "santandercredit.php": {
- "type": "-",
- "size": 59073,
- "lmtime": 1741559739588,
- "modified": false
- },
- "services": {},
- "sql": {},
- "translations": {},
- "upgrade": {},
- "views": {
- "templates": {
- "hook": {
- "displayOrderDetail.tpl": {
- "type": "-",
- "size": 5988,
- "lmtime": 0,
- "modified": false
- },
- "ehpDisplayAdminOrder.tpl": {
- "type": "-",
- "size": 698,
- "lmtime": 0,
- "modified": false
- },
- "ehpDisplayDetails.tpl": {
- "type": "-",
- "size": 2862,
- "lmtime": 0,
- "modified": false
- },
- "ehpDisplayLog.tpl": {
- "type": "-",
- "size": 2857,
- "lmtime": 0,
- "modified": false
- },
- "index.php": {
- "type": "-",
- "size": 1272,
- "lmtime": 0,
- "modified": false
- },
- "infos2.tpl": {
- "type": "-",
- "size": 4834,
- "lmtime": 0,
- "modified": false
- },
- "infos.tpl": {
- "type": "-",
- "size": 5007,
- "lmtime": 0,
- "modified": false
- },
- "santanderCreditInfo.tpl": {
- "type": "-",
- "size": 1154,
- "lmtime": 0,
- "modified": false
- },
- "santanderCreditPayment.tpl": {
- "type": "-",
- "size": 3580,
- "lmtime": 0,
- "modified": false
- },
- "santanderCreditProduct.tpl": {
- "type": "-",
- "size": 1740,
- "lmtime": 1741560111947,
- "modified": false
- }
- }
- }
- }
- },
- "statsbestcategories": {},
- "statsbestcustomers": {},
- "statsbestmanufacturers": {},
- "statsbestproducts": {},
- "statsbestsuppliers": {},
- "statsbestvouchers": {},
- "statscarrier": {},
- "statscatalog": {},
- "statscheckup": {},
- "statsdata": {},
- "statsforecast": {},
- "statsnewsletter": {},
- "statspersonalinfos": {},
- "statsproduct": {},
- "statsregistrations": {},
- "statssales": {},
- "statssearch": {},
- "statsstock": {},
- "welcome": {},
"leobootstrapmenu": {
"classes": {
"BtmegamenuGroup.php": {
@@ -2916,6 +2622,1245 @@
"lmtime": 1736978940000,
"modified": false
}
+ },
+ "pagesnotfound": {},
+ "productcomments": {},
+ "ps_accounts": {},
+ "ps_banner": {},
+ "ps_buybuttonlite": {},
+ "ps_categorytree": {},
+ "ps_checkout": {},
+ "ps_checkpayment": {},
+ "ps_contactinfo": {},
+ "ps_crossselling": {},
+ "ps_currencyselector": {},
+ "ps_customeraccountlinks": {},
+ "ps_customersignin": {},
+ "ps_customtext": {},
+ "ps_dataprivacy": {},
+ "ps_emailalerts": {
+ "composer.json": {
+ "type": "-",
+ "size": 635,
+ "lmtime": 1742566965000,
+ "modified": false
+ },
+ "composer.lock": {
+ "type": "-",
+ "size": 96251,
+ "lmtime": 1742566964000,
+ "modified": false
+ },
+ "config_pl.xml": {
+ "type": "-",
+ "size": 575,
+ "lmtime": 1742566964000,
+ "modified": false
+ },
+ "config.xml": {
+ "type": "-",
+ "size": 604,
+ "lmtime": 1742566964000,
+ "modified": false
+ },
+ "CONTRIBUTORS.md": {
+ "type": "-",
+ "size": 163,
+ "lmtime": 1742566963000,
+ "modified": false
+ },
+ "controllers": {
+ "front": {
+ "account.php": {
+ "type": "-",
+ "size": 2239,
+ "lmtime": 1742566971000,
+ "modified": false
+ },
+ "actions.php": {
+ "type": "-",
+ "size": 5710,
+ "lmtime": 1742566970000,
+ "modified": false
+ },
+ "index.php": {
+ "type": "-",
+ "size": 1127,
+ "lmtime": 1742566971000,
+ "modified": false
+ }
+ },
+ "index.php": {
+ "type": "-",
+ "size": 1127,
+ "lmtime": 1742566970000,
+ "modified": false
+ }
+ },
+ "index.php": {
+ "type": "-",
+ "size": 1127,
+ "lmtime": 1742566965000,
+ "modified": false
+ },
+ "js": {
+ "admin": {
+ "ps_emailalerts.js": {
+ "type": "-",
+ "size": 2225,
+ "lmtime": 1742566968000,
+ "modified": false
+ }
+ },
+ "index.php": {
+ "type": "-",
+ "size": 1127,
+ "lmtime": 1742566968000,
+ "modified": false
+ },
+ "mailalerts.js": {
+ "type": "-",
+ "size": 3411,
+ "lmtime": 1742566968000,
+ "modified": false
+ }
+ },
+ "LICENSE.md": {
+ "type": "-",
+ "size": 10329,
+ "lmtime": 1742566963000,
+ "modified": false
+ },
+ "logo.png": {
+ "type": "-",
+ "size": 4942,
+ "lmtime": 1742566964000,
+ "modified": false
+ },
+ "logo.webp": {
+ "type": "-",
+ "size": 1568,
+ "lmtime": 1742566964000,
+ "modified": false
+ },
+ "MailAlert.php": {
+ "type": "-",
+ "size": 13614,
+ "lmtime": 1742566964000,
+ "modified": false
+ },
+ "mailalerts-account.php": {
+ "type": "-",
+ "size": 2685,
+ "lmtime": 1742566965000,
+ "modified": false
+ },
+ "mailalerts-ajax.php": {
+ "type": "-",
+ "size": 1111,
+ "lmtime": 1742566964000,
+ "modified": false
+ },
+ "mailalerts-extra.php": {
+ "type": "-",
+ "size": 3372,
+ "lmtime": 1742566964000,
+ "modified": false
+ },
+ "mails": {
+ "en": {
+ "customer_qty.html": {
+ "type": "-",
+ "size": 5446,
+ "lmtime": 1742566973000,
+ "modified": false
+ },
+ "customer_qty.txt": {
+ "type": "-",
+ "size": 278,
+ "lmtime": 1742566975000,
+ "modified": false
+ },
+ "index.php": {
+ "type": "-",
+ "size": 1127,
+ "lmtime": 1742566974000,
+ "modified": false
+ },
+ "new_order.html": {
+ "type": "-",
+ "size": 16873,
+ "lmtime": 1742566974000,
+ "modified": false
+ },
+ "new_order.txt": {
+ "type": "-",
+ "size": 606,
+ "lmtime": 1742566973000,
+ "modified": false
+ },
+ "order_changed.html": {
+ "type": "-",
+ "size": 6172,
+ "lmtime": 1742566973000,
+ "modified": false
+ },
+ "order_changed.txt": {
+ "type": "-",
+ "size": 535,
+ "lmtime": 1742566973000,
+ "modified": false
+ },
+ "productcoverage.html": {
+ "type": "-",
+ "size": 5407,
+ "lmtime": 1742566974000,
+ "modified": false
+ },
+ "productcoverage.txt": {
+ "type": "-",
+ "size": 229,
+ "lmtime": 1742566973000,
+ "modified": false
+ },
+ "productoutofstock.html": {
+ "type": "-",
+ "size": 5501,
+ "lmtime": 1742566974000,
+ "modified": false
+ },
+ "productoutofstock.txt": {
+ "type": "-",
+ "size": 303,
+ "lmtime": 1742566974000,
+ "modified": false
+ },
+ "return_slip.html": {
+ "type": "-",
+ "size": 7614,
+ "lmtime": 1742566974000,
+ "modified": false
+ },
+ "return_slip.txt": {
+ "type": "-",
+ "size": 420,
+ "lmtime": 1742566974000,
+ "modified": false
+ }
+ },
+ "pl": {
+ "customer_qty.html": {
+ "type": "-",
+ "size": 26018,
+ "lmtime": 1736978509245,
+ "modified": false
+ },
+ "customer_qty.txt": {
+ "type": "-",
+ "size": 384,
+ "lmtime": 1736978509245,
+ "modified": false
+ },
+ "new_order.html": {
+ "type": "-",
+ "size": 59197,
+ "lmtime": 1736978509246,
+ "modified": false
+ },
+ "new_order.txt": {
+ "type": "-",
+ "size": 832,
+ "lmtime": 1736978509247,
+ "modified": false
+ },
+ "order_changed.html": {
+ "type": "-",
+ "size": 28750,
+ "lmtime": 1736978509247,
+ "modified": false
+ },
+ "order_changed.txt": {
+ "type": "-",
+ "size": 591,
+ "lmtime": 1736978509248,
+ "modified": false
+ },
+ "productcoverage.html": {
+ "type": "-",
+ "size": 22913,
+ "lmtime": 1736978509248,
+ "modified": false
+ },
+ "productcoverage.txt": {
+ "type": "-",
+ "size": 325,
+ "lmtime": 1736978509249,
+ "modified": false
+ },
+ "productoutofstock.html": {
+ "type": "-",
+ "size": 26620,
+ "lmtime": 1736978509249,
+ "modified": false
+ },
+ "productoutofstock.txt": {
+ "type": "-",
+ "size": 433,
+ "lmtime": 1736978509250,
+ "modified": false
+ },
+ "return_slip.html": {
+ "type": "-",
+ "size": 45652,
+ "lmtime": 1736978509250,
+ "modified": false
+ },
+ "return_slip.txt": {
+ "type": "-",
+ "size": 510,
+ "lmtime": 1736978509251,
+ "modified": false
+ }
+ },
+ "sk": {
+ "customer_qty.html": {
+ "type": "-",
+ "size": 5472,
+ "lmtime": 1742566975000,
+ "modified": false
+ },
+ "customer_qty.txt": {
+ "type": "-",
+ "size": 294,
+ "lmtime": 1742566976000,
+ "modified": false
+ },
+ "new_order.html": {
+ "type": "-",
+ "size": 16921,
+ "lmtime": 1742566975000,
+ "modified": false
+ },
+ "new_order.txt": {
+ "type": "-",
+ "size": 630,
+ "lmtime": 1742566975000,
+ "modified": false
+ },
+ "order_changed.html": {
+ "type": "-",
+ "size": 6559,
+ "lmtime": 1742566975000,
+ "modified": false
+ },
+ "order_changed.txt": {
+ "type": "-",
+ "size": 564,
+ "lmtime": 1742566975000,
+ "modified": false
+ },
+ "productcoverage.html": {
+ "type": "-",
+ "size": 5444,
+ "lmtime": 1742566975000,
+ "modified": false
+ },
+ "productcoverage.txt": {
+ "type": "-",
+ "size": 263,
+ "lmtime": 1742566975000,
+ "modified": false
+ },
+ "productoutofstock.html": {
+ "type": "-",
+ "size": 5504,
+ "lmtime": 1742566976000,
+ "modified": false
+ },
+ "productoutofstock.txt": {
+ "type": "-",
+ "size": 308,
+ "lmtime": 1742566976000,
+ "modified": false
+ },
+ "return_slip.html": {
+ "type": "-",
+ "size": 7677,
+ "lmtime": 1742566976000,
+ "modified": false
+ },
+ "return_slip.txt": {
+ "type": "-",
+ "size": 478,
+ "lmtime": 1742566976000,
+ "modified": false
+ }
+ },
+ "index.php": {
+ "type": "-",
+ "size": 1127,
+ "lmtime": 1742566971000,
+ "modified": false
+ }
+ },
+ "ps_emailalerts.php": {
+ "type": "-",
+ "size": 60692,
+ "lmtime": 1742566965000,
+ "modified": false
+ },
+ "README.md": {
+ "type": "-",
+ "size": 1525,
+ "lmtime": 1742566964000,
+ "modified": false
+ },
+ "tests": {
+ "php": {
+ "phpstan": {
+ "phpstan-1.7.6.neon": {
+ "type": "-",
+ "size": 547,
+ "lmtime": 1742566967000,
+ "modified": false
+ },
+ "phpstan-1.7.7.neon": {
+ "type": "-",
+ "size": 323,
+ "lmtime": 1742566967000,
+ "modified": false
+ },
+ "phpstan-1.7.8.neon": {
+ "type": "-",
+ "size": 323,
+ "lmtime": 1742566966000,
+ "modified": false
+ },
+ "phpstan-8.0.neon": {
+ "type": "-",
+ "size": 322,
+ "lmtime": 1742566967000,
+ "modified": false
+ },
+ "phpstan-latest.neon": {
+ "type": "-",
+ "size": 323,
+ "lmtime": 1742566967000,
+ "modified": false
+ },
+ "phpstan.neon": {
+ "type": "-",
+ "size": 440,
+ "lmtime": 1742566967000,
+ "modified": false
+ }
+ },
+ "phpstan.sh": {
+ "type": "-",
+ "size": 1165,
+ "lmtime": 1742566966000,
+ "modified": false
+ }
+ }
+ },
+ "translations": {
+ "index.php": {
+ "type": "-",
+ "size": 1127,
+ "lmtime": 1742566971000,
+ "modified": false
+ },
+ "pl.php": {
+ "type": "-",
+ "size": 0,
+ "lmtime": 1742566970000,
+ "modified": false
+ }
+ },
+ "upgrade": {
+ "index.php": {
+ "type": "-",
+ "size": 1127,
+ "lmtime": 1742566968000,
+ "modified": false
+ },
+ "install-2.1.0.php": {
+ "type": "-",
+ "size": 1095,
+ "lmtime": 1742566967000,
+ "modified": false
+ },
+ "install-2.3.4.php": {
+ "type": "-",
+ "size": 981,
+ "lmtime": 1742566968000,
+ "modified": false
+ },
+ "upgrade-2.3.1.php": {
+ "type": "-",
+ "size": 1039,
+ "lmtime": 1742566968000,
+ "modified": false
+ },
+ "upgrade-2.3.3.php": {
+ "type": "-",
+ "size": 1041,
+ "lmtime": 1742566968000,
+ "modified": false
+ },
+ "upgrade-2.4.0.php": {
+ "type": "-",
+ "size": 1997,
+ "lmtime": 1742566967000,
+ "modified": false
+ },
+ "upgrade-2.4.1.php": {
+ "type": "-",
+ "size": 977,
+ "lmtime": 1742566967000,
+ "modified": false
+ }
+ },
+ "vendor": {
+ "autoload.php": {
+ "type": "-",
+ "size": 178,
+ "lmtime": 1742566965000,
+ "modified": false
+ },
+ "composer": {
+ "autoload_classmap.php": {
+ "type": "-",
+ "size": 147,
+ "lmtime": 1742566966000,
+ "modified": false
+ },
+ "autoload_namespaces.php": {
+ "type": "-",
+ "size": 149,
+ "lmtime": 1742566966000,
+ "modified": false
+ },
+ "autoload_psr4.php": {
+ "type": "-",
+ "size": 143,
+ "lmtime": 1742566966000,
+ "modified": false
+ },
+ "autoload_real.php": {
+ "type": "-",
+ "size": 1453,
+ "lmtime": 1742566966000,
+ "modified": false
+ },
+ "autoload_static.php": {
+ "type": "-",
+ "size": 317,
+ "lmtime": 1742566965000,
+ "modified": false
+ },
+ "ClassLoader.php": {
+ "type": "-",
+ "size": 13420,
+ "lmtime": 1742566966000,
+ "modified": false
+ },
+ "installed.json": {
+ "type": "-",
+ "size": 3,
+ "lmtime": 1742566966000,
+ "modified": false
+ },
+ "LICENSE": {
+ "type": "-",
+ "size": 1070,
+ "lmtime": 1742566965000,
+ "modified": false
+ }
+ }
+ },
+ "views": {
+ "index.php": {
+ "type": "-",
+ "size": 1127,
+ "lmtime": 1742566968000,
+ "modified": false
+ },
+ "templates": {
+ "admin": {
+ "_configure": {
+ "helpers": {
+ "form": {
+ "form.tpl": {
+ "type": "-",
+ "size": 3381,
+ "lmtime": 1742566969000,
+ "modified": false
+ },
+ "index.php": {
+ "type": "-",
+ "size": 1127,
+ "lmtime": 1742566970000,
+ "modified": false
+ }
+ },
+ "index.php": {
+ "type": "-",
+ "size": 1127,
+ "lmtime": 1742566969000,
+ "modified": false
+ }
+ },
+ "index.php": {
+ "type": "-",
+ "size": 1127,
+ "lmtime": 1742566969000,
+ "modified": false
+ }
+ },
+ "index.php": {
+ "type": "-",
+ "size": 1127,
+ "lmtime": 1742566969000,
+ "modified": false
+ }
+ },
+ "front": {
+ "index.php": {
+ "type": "-",
+ "size": 1127,
+ "lmtime": 1742566969000,
+ "modified": false
+ },
+ "mailalerts-account-line.tpl": {
+ "type": "-",
+ "size": 1513,
+ "lmtime": 1742566969000,
+ "modified": false
+ },
+ "mailalerts-account.tpl": {
+ "type": "-",
+ "size": 1427,
+ "lmtime": 1742566969000,
+ "modified": false
+ }
+ },
+ "hook": {
+ "index.php": {
+ "type": "-",
+ "size": 1127,
+ "lmtime": 1742566970000,
+ "modified": false
+ },
+ "my-account-footer.tpl": {
+ "type": "-",
+ "size": 966,
+ "lmtime": 1742566970000,
+ "modified": false
+ },
+ "my-account.tpl": {
+ "type": "-",
+ "size": 966,
+ "lmtime": 1742566970000,
+ "modified": false
+ },
+ "product.tpl": {
+ "type": "-",
+ "size": 2064,
+ "lmtime": 1742566970000,
+ "modified": false
+ }
+ },
+ "index.php": {
+ "type": "-",
+ "size": 1127,
+ "lmtime": 1742566969000,
+ "modified": false
+ }
+ }
+ }
+ },
+ "ps_emailsubscription": {},
+ "ps_facebook": {},
+ "ps_facetedsearch": {
+ "_dev": {
+ "back": {
+ "blocklayered.css": {
+ "type": "-",
+ "size": 1577,
+ "lmtime": 1737287962008,
+ "modified": false
+ },
+ "blocklayered.css.map": {
+ "type": "-",
+ "size": 4300,
+ "lmtime": 1737287962008,
+ "modified": false
+ },
+ "blocklayered.scss": {
+ "type": "-",
+ "size": 1810,
+ "lmtime": 0,
+ "modified": false
+ },
+ "index.js": {
+ "type": "-",
+ "size": 8041,
+ "lmtime": 0,
+ "modified": false
+ }
+ },
+ "front": {
+ "events.js": {
+ "type": "-",
+ "size": 1114,
+ "lmtime": 0,
+ "modified": false
+ },
+ "facet.css": {
+ "type": "-",
+ "size": 1529,
+ "lmtime": 1737287962008,
+ "modified": false
+ },
+ "facet.css.map": {
+ "type": "-",
+ "size": 3483,
+ "lmtime": 1737287962008,
+ "modified": false
+ },
+ "facet.scss": {
+ "type": "-",
+ "size": 1398,
+ "lmtime": 0,
+ "modified": false
+ },
+ "index.js": {
+ "type": "-",
+ "size": 911,
+ "lmtime": 0,
+ "modified": false
+ },
+ "overlay.css": {
+ "type": "-",
+ "size": 1560,
+ "lmtime": 1737287962008,
+ "modified": false
+ },
+ "overlay.css.map": {
+ "type": "-",
+ "size": 3763,
+ "lmtime": 1737287962008,
+ "modified": false
+ },
+ "overlay.js": {
+ "type": "-",
+ "size": 1225,
+ "lmtime": 0,
+ "modified": false
+ },
+ "overlay.scss": {
+ "type": "-",
+ "size": 1522,
+ "lmtime": 0,
+ "modified": false
+ },
+ "slider.css": {
+ "type": "-",
+ "size": 1194,
+ "lmtime": 1737287962008,
+ "modified": false
+ },
+ "slider.css.map": {
+ "type": "-",
+ "size": 2760,
+ "lmtime": 1737287962008,
+ "modified": false
+ },
+ "slider.js": {
+ "type": "-",
+ "size": 3816,
+ "lmtime": 0,
+ "modified": false
+ },
+ "slider.scss": {
+ "type": "-",
+ "size": 1179,
+ "lmtime": 0,
+ "modified": false
+ },
+ "urlparser.js": {
+ "type": "-",
+ "size": 1051,
+ "lmtime": 0,
+ "modified": false
+ }
+ }
+ }
+ },
+ "ps_faviconnotificationbo": {},
+ "ps_featuredproducts": {},
+ "psgdpr": {
+ "views": {
+ "templates": {
+ "hook": {
+ "displayGDPRConsent.tpl": {
+ "type": "-",
+ "size": 4075,
+ "lmtime": 1737912422958,
+ "modified": false
+ }
+ }
+ }
+ }
+ },
+ "ps_imageslider": {},
+ "ps_languageselector": {},
+ "ps_linklist": {},
+ "ps_mainmenu": {},
+ "ps_mbo": {},
+ "ps_metrics": {},
+ "ps_reminder": {},
+ "ps_searchbar": {},
+ "ps_sharebuttons": {},
+ "ps_shoppingcart": {},
+ "ps_socialfollow": {},
+ "ps_themecusto": {},
+ "ps_wirepayment": {},
+ "psxmarketingwithgoogle": {},
+ "raty": {},
+ "referralprogram": {},
+ "regenerateimage": {},
+ "santandercredit": {
+ "CERT": {},
+ "config": {},
+ "config_pl.xml": {
+ "type": "-",
+ "size": 507,
+ "lmtime": 0,
+ "modified": false
+ },
+ "config.xml": {
+ "type": "-",
+ "size": 579,
+ "lmtime": 0,
+ "modified": false
+ },
+ "controllers": {},
+ "doc": {},
+ "images": {},
+ "index.php": {
+ "type": "-",
+ "size": 1272,
+ "lmtime": 0,
+ "modified": false
+ },
+ "js": {},
+ "logo.gif": {
+ "type": "-",
+ "size": 1217,
+ "lmtime": 0,
+ "modified": false
+ },
+ "logo.png": {
+ "type": "-",
+ "size": 3571,
+ "lmtime": 0,
+ "modified": false
+ },
+ "santandercredit.php": {
+ "type": "-",
+ "size": 59073,
+ "lmtime": 1741559739588,
+ "modified": false
+ },
+ "services": {},
+ "sql": {},
+ "translations": {},
+ "upgrade": {},
+ "views": {
+ "templates": {
+ "hook": {
+ "displayOrderDetail.tpl": {
+ "type": "-",
+ "size": 5988,
+ "lmtime": 0,
+ "modified": false
+ },
+ "ehpDisplayAdminOrder.tpl": {
+ "type": "-",
+ "size": 698,
+ "lmtime": 0,
+ "modified": false
+ },
+ "ehpDisplayDetails.tpl": {
+ "type": "-",
+ "size": 2862,
+ "lmtime": 0,
+ "modified": false
+ },
+ "ehpDisplayLog.tpl": {
+ "type": "-",
+ "size": 2857,
+ "lmtime": 0,
+ "modified": false
+ },
+ "index.php": {
+ "type": "-",
+ "size": 1272,
+ "lmtime": 0,
+ "modified": false
+ },
+ "infos2.tpl": {
+ "type": "-",
+ "size": 4834,
+ "lmtime": 0,
+ "modified": false
+ },
+ "infos.tpl": {
+ "type": "-",
+ "size": 5007,
+ "lmtime": 0,
+ "modified": false
+ },
+ "santanderCreditInfo.tpl": {
+ "type": "-",
+ "size": 1154,
+ "lmtime": 0,
+ "modified": false
+ },
+ "santanderCreditPayment.tpl": {
+ "type": "-",
+ "size": 3580,
+ "lmtime": 0,
+ "modified": false
+ },
+ "santanderCreditProduct.tpl": {
+ "type": "-",
+ "size": 1740,
+ "lmtime": 1741560111947,
+ "modified": false
+ }
+ }
+ }
+ }
+ },
+ "statsbestcategories": {},
+ "statsbestcustomers": {},
+ "statsbestmanufacturers": {},
+ "statsbestproducts": {},
+ "statsbestsuppliers": {},
+ "statsbestvouchers": {},
+ "statscarrier": {},
+ "statscatalog": {},
+ "statscheckup": {},
+ "statsdata": {},
+ "statsforecast": {},
+ "statsnewsletter": {},
+ "statspersonalinfos": {},
+ "statsproduct": {},
+ "statsregistrations": {},
+ "statssales": {},
+ "statssearch": {},
+ "statsstock": {},
+ "welcome": {},
+ "ps_cashondelivery": {
+ "composer.json": {
+ "type": "-",
+ "size": 1072,
+ "lmtime": 1742566353000,
+ "modified": false
+ },
+ "composer.lock": {
+ "type": "-",
+ "size": 51498,
+ "lmtime": 1742566352000,
+ "modified": false
+ },
+ "config_pl.xml": {
+ "type": "-",
+ "size": 482,
+ "lmtime": 1742566352000,
+ "modified": false
+ },
+ "config.xml": {
+ "type": "-",
+ "size": 496,
+ "lmtime": 1742566352000,
+ "modified": false
+ },
+ "controllers": {
+ "front": {
+ "index.php": {
+ "type": "-",
+ "size": 1127,
+ "lmtime": 1742566357000,
+ "modified": false
+ },
+ "validation.php": {
+ "type": "-",
+ "size": 4122,
+ "lmtime": 1742566357000,
+ "modified": false
+ }
+ },
+ "index.php": {
+ "type": "-",
+ "size": 1127,
+ "lmtime": 1742566357000,
+ "modified": false
+ }
+ },
+ "index.php": {
+ "type": "-",
+ "size": 1127,
+ "lmtime": 1742566352000,
+ "modified": false
+ },
+ "LICENSE.md": {
+ "type": "-",
+ "size": 10329,
+ "lmtime": 1742566351000,
+ "modified": false
+ },
+ "logo.png": {
+ "type": "-",
+ "size": 8755,
+ "lmtime": 1742566352000,
+ "modified": false
+ },
+ "logo.webp": {
+ "type": "-",
+ "size": 716,
+ "lmtime": 1742566352000,
+ "modified": false
+ },
+ "ps_cashondelivery.php": {
+ "type": "-",
+ "size": 12104,
+ "lmtime": 1742566352000,
+ "modified": false
+ },
+ "README.md": {
+ "type": "-",
+ "size": 863,
+ "lmtime": 1742567048000,
+ "modified": false
+ },
+ "tests": {
+ "index.php": {
+ "type": "-",
+ "size": 1271,
+ "lmtime": 1742566354000,
+ "modified": false
+ },
+ "phpstan": {
+ "index.php": {
+ "type": "-",
+ "size": 1271,
+ "lmtime": 1742566354000,
+ "modified": false
+ },
+ "phpstan-1.7.6.neon": {
+ "type": "-",
+ "size": 799,
+ "lmtime": 1742566355000,
+ "modified": false
+ },
+ "phpstan-1.7.7.neon": {
+ "type": "-",
+ "size": 416,
+ "lmtime": 1742566355000,
+ "modified": false
+ },
+ "phpstan-1.7.8.neon": {
+ "type": "-",
+ "size": 185,
+ "lmtime": 1742566354000,
+ "modified": false
+ },
+ "phpstan-latest.neon": {
+ "type": "-",
+ "size": 185,
+ "lmtime": 1742566354000,
+ "modified": false
+ },
+ "phpstan.neon": {
+ "type": "-",
+ "size": 341,
+ "lmtime": 1742566354000,
+ "modified": false
+ }
+ },
+ "phpstan.sh": {
+ "type": "-",
+ "size": 1074,
+ "lmtime": 1742566354000,
+ "modified": false
+ }
+ },
+ "translations": {
+ "index.php": {
+ "type": "-",
+ "size": 1127,
+ "lmtime": 1742566357000,
+ "modified": false
+ },
+ "pl.php": {
+ "type": "-",
+ "size": 0,
+ "lmtime": 1742566356000,
+ "modified": false
+ }
+ },
+ "upgrade": {
+ "index.php": {
+ "type": "-",
+ "size": 1127,
+ "lmtime": 1742566355000,
+ "modified": false
+ },
+ "upgrade-1.0.7.php": {
+ "type": "-",
+ "size": 1252,
+ "lmtime": 1742566355000,
+ "modified": false
+ },
+ "upgrade-2.0.0.php": {
+ "type": "-",
+ "size": 1906,
+ "lmtime": 1742566355000,
+ "modified": false
+ }
+ },
+ "vendor": {
+ "autoload.php": {
+ "type": "-",
+ "size": 178,
+ "lmtime": 1742566353000,
+ "modified": false
+ },
+ "composer": {
+ "autoload_classmap.php": {
+ "type": "-",
+ "size": 317,
+ "lmtime": 1742566354000,
+ "modified": false
+ },
+ "autoload_namespaces.php": {
+ "type": "-",
+ "size": 149,
+ "lmtime": 1742566354000,
+ "modified": false
+ },
+ "autoload_psr4.php": {
+ "type": "-",
+ "size": 143,
+ "lmtime": 1742566354000,
+ "modified": false
+ },
+ "autoload_real.php": {
+ "type": "-",
+ "size": 1453,
+ "lmtime": 1742566353000,
+ "modified": false
+ },
+ "autoload_static.php": {
+ "type": "-",
+ "size": 656,
+ "lmtime": 1742566353000,
+ "modified": false
+ },
+ "ClassLoader.php": {
+ "type": "-",
+ "size": 13420,
+ "lmtime": 1742566353000,
+ "modified": false
+ },
+ "installed.json": {
+ "type": "-",
+ "size": 3,
+ "lmtime": 1742566353000,
+ "modified": false
+ },
+ "LICENSE": {
+ "type": "-",
+ "size": 1070,
+ "lmtime": 1742566353000,
+ "modified": false
+ }
+ }
+ },
+ "views": {
+ "img": {
+ "index.php": {
+ "type": "-",
+ "size": 1127,
+ "lmtime": 1742566355000,
+ "modified": false
+ },
+ "orderstate": {
+ "index.php": {
+ "type": "-",
+ "size": 1127,
+ "lmtime": 1742566356000,
+ "modified": false
+ },
+ "PS_OS_COD_VALIDATION.gif": {
+ "type": "-",
+ "size": 1028,
+ "lmtime": 1742566356000,
+ "modified": false
+ }
+ }
+ },
+ "index.php": {
+ "type": "-",
+ "size": 1127,
+ "lmtime": 1742566355000,
+ "modified": false
+ },
+ "templates": {
+ "hook": {
+ "displayOrderConfirmation.tpl": {
+ "type": "-",
+ "size": 1390,
+ "lmtime": 1742566356000,
+ "modified": false
+ },
+ "index.php": {
+ "type": "-",
+ "size": 1127,
+ "lmtime": 1742566356000,
+ "modified": false
+ },
+ "paymentOptions-additionalInformation.tpl": {
+ "type": "-",
+ "size": 999,
+ "lmtime": 1742566356000,
+ "modified": false
+ },
+ "payment_return.tpl": {
+ "type": "-",
+ "size": 1470,
+ "lmtime": 1742566356000,
+ "modified": false
+ },
+ "ps_cashondelivery_intro.tpl": {
+ "type": "-",
+ "size": 1090,
+ "lmtime": 1742566356000,
+ "modified": false
+ }
+ },
+ "index.php": {
+ "type": "-",
+ "size": 1127,
+ "lmtime": 1742566356000,
+ "modified": false
+ }
+ }
+ }
}
},
"override": {},
diff --git a/modules/import_api/classes/convertor.php b/modules/import_api/classes/convertor.php
new file mode 100644
index 00000000..8986f9dd
--- /dev/null
+++ b/modules/import_api/classes/convertor.php
@@ -0,0 +1,448 @@
+
+ * @copyright 2019 Dalibor Stojcevski
+ * @license Dalibor Stojcevski
+ */
+
+class Convertor
+{
+ private $settings;
+ private $creator;
+ private $id_lang;
+
+ public function __construct($settings, $creator)
+ {
+ $this->settings = $settings;
+ $this->creator = $creator;
+ $this->id_lang = (int)Configuration::get('PS_LANG_DEFAULT');
+ }
+
+ public function convertToFrmProduct($original_product)
+ {
+ $product_category = array();
+ if (!empty($original_product['category_path'])) {
+
+ foreach ($original_product['category_path'] as $path) {
+
+ if ($path['parent']) {
+ $parent_id = $this->creator->getCategoryId($path['parent'], $this->settings['top_category_id']);
+ } else {
+ $parent_id = $this->settings['top_category_id'];
+ }
+
+ if ($this->settings['import_api_settings']['category_path']) {
+ $categories = explode('->', $path['value']);
+ } else {
+ $categories = array($path['value']);
+ }
+
+ foreach ($categories as $category) {
+ if (!$category) {
+ continue;
+ }
+
+ $parent_id = $category_id = $this->creator->getCategoryId($category, $parent_id);
+
+ }
+
+ $product_category[] = $category_id;
+ }
+ }
+
+ $product_category = array_unique($product_category);
+
+ if (!$product_category && $this->settings['default_category_id']) {
+ $product_category[] = $this->settings['default_category_id'];
+ }
+
+ if (!$product_category && $this->settings['top_category_id']) {
+ $product_category[] = $this->settings['top_category_id'];
+ }
+
+ if (!in_array(Configuration::get('PS_HOME_CATEGORY'),$product_category)) {
+ //$product_category[] = Configuration::get('PS_HOME_CATEGORY');
+ }
+
+ if (!$product_category) {
+ $product_category[] = Configuration::get('PS_HOME_CATEGORY');
+ }
+
+ if (!empty($original_product['brand'])) {
+ $manufacturer_id = $this->creator->getManufacturerId($original_product['brand']);
+ } else {
+ $manufacturer_id = $this->settings['default_manufacturer_id'];
+ }
+
+
+
+ $product = new Product();
+
+ $original_product['description'] = nl2br($original_product['description']);
+
+
+
+ foreach (Language::getLanguages(false) as $lang) {
+ $original_product['name']= Tools::cleanNonUnicodeSupport($original_product['name']);
+ $product->name[$lang['id_lang']] = htmlspecialchars_decode($original_product['name']);
+
+ $product->description_long[$lang['id_lang']] = htmlspecialchars_decode(htmlspecialchars_decode($original_product['description'], ENT_COMPAT));
+ $product->description[$lang['id_lang']] = htmlspecialchars_decode(htmlspecialchars_decode($original_product['description'], ENT_COMPAT));
+ $return_str = $original_product['name'];
+ $return_str = Tools::replaceAccentedChars($return_str); //AccentedChars
+ $return_str = preg_replace('/[^a-zA-Z0-9\s\'\:\/\[\]\-]/', '', $return_str); // accented chars
+ $product->link_rewrite[$lang['id_lang']] = Tools::link_rewrite($return_str);
+ $product->meta_keywords[$lang['id_lang']] = str_replace(' ', ',', $original_product['name']);
+
+ if (!empty($original_product['short_description'])) {
+ $product->description_short[$lang['id_lang']] = $original_product['short_description'];
+ }
+ }
+
+ $product->reference = $original_product['reference'];
+ $product->id_category = $product_category;
+ $product->id_manufacturer = $manufacturer_id;
+ $product->id_category_default = !empty($category_id) ? $category_id : Configuration::get('PS_HOME_CATEGORY');
+ $product->active = isset($original_product['active']) ? $original_product['active'] : 1;
+ $product->width = $original_product['width'];
+ $product->height = $original_product['height'];
+ $product->depth = $original_product['depth'];
+ $product->weight = $original_product['weight'];
+ //'visibility' => 'both',
+ $product->location = $original_product['location'];
+ $product->additional_shipping_cost = $original_product['additional_shipping_cost'];
+ //'unit_price' => 0,
+
+ $product->quantity = $original_product['quantity'];
+ $product->minimal_quantity = isset($original_product['minimal_quantity']) ? $original_product['minimal_quantity'] : 1;
+ $product->price = $original_product['price'];
+ $product->wholesale_price = $original_product['wholesale_price'];
+ $product->reference = $original_product['reference'];
+ $product->ean13 = $original_product['ean13'];
+ $product->upc = $original_product['upc'];
+ $product->condition = $original_product['condition'];
+
+ if ($this->settings['id_tax_rules_group'] != -1) {
+ $product->id_tax_rules_group = $this->settings['id_tax_rules_group'];
+ }
+
+ $product -> additional_delivery_times = 2;
+ $product->delivery_in_stock[7] = '1-5 dni roboczych';
+
+ $product->add();
+
+ // Pobieranie listy wszystkich sklepów
+ $shops = Shop::getShops();
+ foreach ($shops as $shop) {
+ // Skojarzenie produktu ze sklepem
+ $product->associateTo($shop['id_shop']);
+ }
+
+ // Aktualizacja danych produktu dla każdego sklepu
+ foreach ($shops as $shop) {
+ Shop::setContext(Shop::CONTEXT_SHOP, $shop['id_shop']);
+
+ // Przykład aktualizacji stanu magazynowego, statusu i zdjęć
+ StockAvailable::setQuantity($product->id, 0, $product->quantity, $shop['id_shop']);
+ $product->active = 1; // Ustawienie produktu jako aktywnego
+ $product->update();
+
+ // Tutaj możesz dodać logikę do aktualizacji zdjęć produktu, jeśli to konieczne
+ // Może to wymagać dodatkowego kodu zależnego od sposobu zarządzania zdjęciami w Twoim sklepie
+ }
+
+ // Resetowanie kontekstu sklepu (opcjonalnie)
+ Shop::setContext(Shop::CONTEXT_ALL);
+
+ foreach ($original_product['images'] as $image_url) {
+ $this->creator->addImageToProduct($image_url, $product->id);
+ }
+
+ if (!empty($original_product['cover'])) {
+ $this->creator->addImageToProduct($original_product['cover'], $product->id, true);
+ }
+
+ if ( $original_product['feature'] and $original_product['feature_value'] ) {
+ $original_product['features'] = array(
+ array(
+ 'feature' => $original_product['feature'],
+ 'feature_value' => $original_product['feature_value']
+ )
+ );
+ }
+
+ if (!empty($original_product['features'])) {
+ $this->creator->addFeaturesToProduct($original_product['features'], $product->id);
+ }
+
+ StockAvailable::setQuantity($product->id, 0, $original_product['quantity']);
+
+ if (!empty($original_product['attributes'])) {
+ $attribute_details = !empty($original_product['attribute_details']) ? $original_product['attribute_details'] : array();
+
+ $this->creator->addAttributesToProduct($original_product['attributes'], $product->id, $attribute_details);
+ }
+
+ $product->addToCategories($product_category);
+
+ return $product;
+ }
+
+ public function unArray($original_product)
+ {
+ $frm_simple_fields = array('unique'=> 'unique_default', 'reference' => '', 'name' => '', 'description' => '', 'brand' => '', 'price' => 0, 'wholesale_price' => 0, 'minimal_quantity' => 1, 'quantity' => '', 'cover' => '', 'ean13' => '', 'upc' => '', 'condition' => 'new', 'additional_shipping_cost' => 0, 'location'=> '', 'width' => 0.00000, 'height' => 0.00000, 'depth' => 0.00000, 'weight' => 0.00000, 'active' => 1);
+
+ foreach ($frm_simple_fields as $field => $default) {
+
+ if (isset($original_product[$field])) {
+ while (is_array($original_product[$field])) {
+ $original_product[$field] = current($original_product[$field]);
+ }
+ } else {
+ $original_product[$field] = $default;
+ }
+ }
+ return $original_product;
+ }
+
+ public function clearInput($original_product)
+ {
+
+
+ $original_product['price'] = (float)str_replace(',', '.', $original_product['price']);
+ $original_product['wholesale_price'] = (float)str_replace(',', '.', $original_product['wholesale_price']);
+ $original_product['additional_shipping_cost'] = (float)str_replace(',', '.', $original_product['additional_shipping_cost']);
+ $original_product['quantity'] = (int)str_replace(',', '.', $original_product['quantity']);
+ $original_product['minimal_quantity'] = (int)str_replace(',', '', $original_product['minimal_quantity']);
+
+ $original_product['width'] = (float)str_replace(',', '.', $original_product['width']);
+ $original_product['height'] = (float)str_replace(',', '.', $original_product['height']);
+ $original_product['depth'] = (float)str_replace(',', '.', $original_product['depth']);
+ $original_product['weight'] = (float)str_replace(',', '.', $original_product['weight']);
+
+ $original_product['price'] = number_format($original_product['price'], 2, '.', '');
+ $original_product['wholesale_price'] = number_format($original_product['wholesale_price'], 2, '.', '');
+ $original_product['additional_shipping_cost'] = number_format($original_product['additional_shipping_cost'], 2, '.', '');
+
+
+ if (isset($original_product['active'])) {
+ $not_active_statuses = array('disabled', 'not active', 'removed'); // lower case, if you add new status
+ if (empty($original_product['active']) || in_array(mb_strtolower($original_product['active']), $not_active_statuses)){
+ $original_product['active'] = 0;
+ } else {
+ $original_product['active'] = 1;
+ }
+ }
+
+ if ($this->settings['import_api_settings']['price_multiplier']) {
+ $original_product['price'] *= $this->settings['import_api_settings']['price_multiplier'];
+ }
+
+
+ if (empty($original_product['name'])) {
+ $original_product['name'] = $original_product['unique'];
+ }
+
+ $original_product['name'] = str_replace(['>','<', '=', ';', '{', '}', '#'], ' ', $original_product['name']);
+
+ $original_product['description'] = htmlspecialchars($original_product['description'], ENT_COMPAT, 'UTF-8'); // This is for to keep html in database after pSQL
+
+ if (!empty($original_product['images'])) {
+ if (!is_array($original_product['images'])) {
+ $original_product['images'] = array($original_product['images']);
+ }
+ } else {
+ $original_product['images'] = array();
+ }
+
+ /*$existing_images = array();
+
+ foreach($original_product['images'] as $img) {
+ $headers = @get_headers($img, 1);
+
+ if (!isset($headers['Content-Type'])) {
+ $existing_images[] = $img;
+ } elseif (strpos($headers['Content-Type'], 'image/') !== FALSE) {
+ // regular image (and not 404).
+ $existing_images[] = $img;
+ }
+ }
+
+ $original_product['images'] = $existing_images;
+
+ $headers = @get_headers($original_product['cover'], 1);
+
+ if (isset($headers['Content-Type']) && strpos($headers['Content-Type'], 'image/') === FALSE) {
+ unset($original_product['cover']);
+ } */
+
+ if (empty($original_product['cover']) && !empty($original_product['images'])) {
+ $original_product['cover'] = array_shift($original_product['images']);
+ }
+
+ if (!empty($original_product['ean13']) && !Validate::isEan13($original_product['ean13'])) {
+ $original_product['ean13'] = '';
+ }
+
+ if (!empty($original_product['upc']) && !Validate::isUpc($original_product['upc'])) {
+ $original_product['upc'] = '';
+ }
+
+ if (!empty($original_product['width']) && !Validate::isUnsignedFloat($original_product['width'])) {
+ $original_product['width'] = 0;
+ }
+
+ if (!empty($original_product['height']) && !Validate::isUnsignedFloat($original_product['height'])) {
+ $original_product['height'] = 0;
+ }
+
+ if (!empty($original_product['depth']) && !Validate::isUnsignedFloat($original_product['depth'])) {
+ $original_product['depth'] = 0;
+ }
+
+ if (!empty($original_product['weight']) && !Validate::isUnsignedFloat($original_product['weight'])) {
+ $original_product['weight'] = 0;
+ }
+
+ if (!empty($original_product['price']) && !Validate::isPrice($original_product['price'])) {
+ $original_product['price'] = 0;
+ }
+
+ if (!empty($original_product['wholesale_price']) && !Validate::isPrice($original_product['wholesale_price'])) {
+ $original_product['wholesale_price'] = 0;
+ }
+
+ if (!empty($original_product['additional_shipping_cost']) && !Validate::isPrice($original_product['additional_shipping_cost'])) {
+ $original_product['additional_shipping_cost'] = 0;
+ }
+
+ if (!empty($original_product['location']) && !Validate::isReference($original_product['location'])) {
+ $original_product['location'] = '';
+ }
+ if (!empty($original_product['short_description']) && !Validate::isCleanHtml($original_product['short_description'])) {
+ $original_product['short_description'] = '';
+ }
+ if (!empty($original_product['description']) && !Validate::isCleanHtml($original_product['description'])) {
+ $original_product['description'] = '';
+ }
+
+ if (!empty($original_product['condition']) && !in_array($original_product['condition'], ['new', 'used', 'refurbished'])) {
+ $original_product['condition'] = '';
+ }
+
+ return $original_product;
+ }
+
+ public function replace($original_product)
+ {
+ foreach ($this->settings['import_api_replace'] as $product_key => $replaces) {
+ foreach ($replaces as $replace) {
+ if (isset($replace[0]) && isset($replace[1])) {
+ if (isset($original_product[$product_key]) && is_string($original_product[$product_key])) {
+ $original_product[$product_key] = str_replace($replace[0], $replace[1], $original_product[$product_key]);
+ }
+ if ($product_key == 'category' && isset($original_product['category_path'])) {
+ foreach($original_product['category_path'] as &$category_path) {
+ if ($category_path['value'] == $replace[0]) {
+ $category_path['value'] = $replace[1];
+ }
+ }
+ }
+ }
+
+ }
+ }
+
+ return $original_product;
+
+ }
+ public function filter($original_product)
+ {
+ $original_product['belong'] = true;
+ foreach ($this->settings['import_api_filter_options'] as $product_key => $filter_option) {
+ if ($filter_option === 'not_empty' && (!isset($original_product[$product_key]) || $original_product[$product_key] === '' || $original_product[$product_key] === 'false' || $original_product[$product_key] === 'null')) {
+ $original_product['belong'] = false;
+ return $original_product;
+ }
+
+ }
+
+ $product_categories = array();
+ if (isset($original_product['category_path'])) {
+ foreach($original_product['category_path'] as $category_path) {
+ $product_categories[] = $category_path['value'];
+ }
+ }
+ foreach ($this->settings['import_api_filter'] as $product_key => $filters) {
+ if (isset($this->settings['import_api_filter_options'][$product_key])) {
+ $search_type = $this->settings['import_api_filter_options'][$product_key];
+ } else {
+ $search_type = 'equal';
+ }
+
+ foreach ($filters as $filter) {
+ if ($filter != '') {
+ if (isset($original_product[$product_key])) {
+ if ($search_type == 'equal') {
+ if (!is_array($original_product[$product_key]) && $original_product[$product_key] == $filter) {
+ $original_product['belong'] = true;
+ continue 2; // Search next product[key] condition, this is found.// If you need to fulfil only one product key condition, exit here from function with belong=true
+
+ }
+ if ($product_key == 'category' && in_array($filter, $product_categories)) {
+ $original_product['belong'] = true;
+ continue 2;
+
+ }
+ } elseif ($search_type == 'not_equal') {
+ if (!is_array($original_product[$product_key]) && $original_product[$product_key] != $filter) {
+ $original_product['belong'] = true;
+ continue 2; // Search next product[key] condition, this is found.// If you need to fulfil only one product key condition, exit here from function with belong=true
+
+ }
+ if ($product_key == 'category' && !in_array($filter, $product_categories)) {
+ $original_product['belong'] = true;
+ continue 2;
+
+ }
+ } elseif ($search_type == 'greater') {
+ if (!is_array($original_product[$product_key]) && $original_product[$product_key] > $filter) {
+ $original_product['belong'] = true;
+ continue 2;
+ }
+ } elseif ($search_type == 'less') {
+ if (!is_array($original_product[$product_key]) && $original_product[$product_key] < $filter) {
+ $original_product['belong'] = true;
+ continue 2;
+ }
+ } elseif ($search_type == 'regexp') {
+ if (!is_array($original_product[$product_key]) && preg_match('/' . $filter .'/i', $original_product[$product_key]) !== 0) {
+ $original_product['belong'] = true;
+ continue 2;
+ }
+
+ if ($product_key == 'category' && preg_grep('/' . $filter .'/i', $product_categories)) {
+ $original_product['belong'] = true;
+ continue 2;
+
+ }
+ }
+ }
+ }
+
+ }
+ $original_product['belong'] = false;// If is here, it is not true. Search is not found
+ break;
+
+ }
+
+ return $original_product;
+
+ }
+}
diff --git a/modules/import_api/classes/creator.php b/modules/import_api/classes/creator.php
new file mode 100644
index 00000000..92aa7e26
--- /dev/null
+++ b/modules/import_api/classes/creator.php
@@ -0,0 +1,851 @@
+
+ * @copyright 2019 Dalibor Stojcevski
+ * @license Dalibor Stojcevski
+ */
+
+/** Used for validatefields diying without user friendly error or not */
+
+if (!defined('UNFRIENDLY_ERROR'))
+{
+ define('UNFRIENDLY_ERROR', false);
+}
+
+class Creator
+{
+
+ private $home_category = 0;
+ private $id_lang = 1;
+
+ public function __construct($file_id)
+ {
+ $this->id_lang = (int)Configuration::get('PS_LANG_DEFAULT');
+ $this->home_category = Configuration::get('PS_HOME_CATEGORY');
+ //$query_file_settings = Db::getInstance()->executeS("SELECT f.*, fs.mapping FROM ". _DB_PREFIX_ . "ia_files f LEFT JOIN ". _DB_PREFIX_ . "ia_file_settings fs ON(fs.file_id = f.file_id) WHERE f.file_id = '" . $file_id . "' LIMIT 1");
+ $query_file_settings = Db::getInstance()->executeS("SELECT * FROM " . _DB_PREFIX_ . "ia_file_settings WHERE file_id = '" . $file_id . "' LIMIT 1");
+ $this->settings = !empty($query_file_settings[0]['settings']) ? json_decode($query_file_settings[0]['settings'], true) : array();
+ $this->setAttributes();
+ }
+
+ public function getManufacturerId($name)
+ {
+ if (is_numeric($name) && Manufacturer::manufacturerExists((int)$name))
+ {
+ return (int)$name;
+ }
+ elseif (is_string($name))
+ {
+ if ($manufacturer_id = Manufacturer::getIdByName($name))
+ {
+ return (int)$manufacturer_id;
+ }
+ else
+ {
+ $manufacturer = new Manufacturer();
+ $manufacturer->name = $name;
+ $manufacturer->active = true;
+
+ if (($field_error = $manufacturer->validateFields(UNFRIENDLY_ERROR, true)) === true &&
+ ($lang_field_error = $manufacturer->validateFieldsLang(UNFRIENDLY_ERROR, true)) === true && $manufacturer->add()
+ )
+ {
+ return (int)$manufacturer->id;
+ //$manufacturer->associateTo($product->id_shop_list);
+ }
+ }
+ }
+
+ return 0;
+ }
+
+ public function getCategoryId($value, $parent_id = null)
+ {
+ if (!$parent_id)
+ {
+ $parent_id = $this->home_category;
+ }
+
+ if (is_numeric($value) && Category::categoryExists((int)$value))
+ {
+ return (int)$value;
+ }
+ elseif (is_string($value))
+ {
+ $existing_category = Category::searchByName($this->id_lang, $value, true, true); // try and false at end to work with Cache, view Category class function
+ if ($existing_category)
+ {
+
+ return (int)$existing_category['id_category'];
+ }
+ else
+ {
+ $category_to_create = new Category();
+ $category_to_create->id = (int)$value;
+ $category_to_create->name = Creator::createMultiLangField($value);
+ $category_to_create->active = 1;
+ $category_to_create->id_parent = $parent_id; // Default parent is home for unknown category to create
+ $category_link_rewrite = Tools::link_rewrite($category_to_create->name[$this->id_lang]);
+ $category_to_create->link_rewrite = Creator::createMultiLangField($category_link_rewrite);
+ if (($field_error = $category_to_create->validateFields(UNFRIENDLY_ERROR, true)) === true &&
+ ($lang_field_error = $category_to_create->validateFieldsLang(UNFRIENDLY_ERROR, true)) === true && $category_to_create->add()
+ )
+ {
+ return (int)$category_to_create->id;
+ }
+ }
+ }
+
+ return 0;
+ }
+
+ protected static function createMultiLangField($field)
+ {
+ $res = array();
+ foreach (Language::getIDs(false) as $id_lang)
+ {
+ $res[$id_lang] = $field;
+ }
+
+ return $res;
+ }
+
+ public function addImageToProduct($url, $id, $is_cover = false)
+ {
+ if (!empty($url))
+ {
+ $url = str_replace(' ', '%20', $url);
+
+ $image = new Image();
+ $image->id_product = (int)$id;
+ $image->position = Image::getHighestPosition($id) + 1;
+ $image->cover = $is_cover;
+ // file_exists doesn't work with HTTP protocol
+ if (($field_error = $image->validateFields(UNFRIENDLY_ERROR, true)) === true &&
+ ($lang_field_error = $image->validateFieldsLang(UNFRIENDLY_ERROR, true)) === true && $image->add()
+ )
+ {
+ // associate image to selected shops
+ $shops = Shop::getContextListShopID();
+ $image->associateTo($shops);
+ if (!Creator::copyImg($id, $image->id, $url, 'products', true))
+ {
+ $image->delete();
+ }
+ }
+ }
+ }
+
+ public function addFeaturesToProduct($features, $id)
+ {
+ foreach ($features as $single_feature)
+ {
+ if (empty($single_feature))
+ {
+ continue;
+ }
+
+ $feature_name = $single_feature['feature'];
+ $feature_value = $single_feature['feature_value'];
+ $position = false;
+ $custom = false;
+ $id_lang = $this->id_lang;
+ $id_product = (int)$id;
+ if (!empty($feature_name) && !empty($feature_value))
+ {
+ $id_feature = (int)Feature::addFeatureImport($feature_name, $position);
+ $id_feature_value = (int)FeatureValue::addFeatureValueImport($id_feature, $feature_value, $id_product, $id_lang, $custom);
+ Product::addFeatureProductImport($id, $id_feature, $id_feature_value);
+ }
+ }
+ }
+
+ public function addAttributesToProduct($attributes_array, $id, $attribute_details = array())
+ {
+
+ $product = new Product($id);
+
+ $this->settings['combination_price_multiplier'];
+
+ $attributes_to_add = array();
+ $attributes_ids = array();
+
+ foreach ($attributes_array as $key => $attributes_full)
+ {
+ foreach ($attributes_full as $attribute_pair)
+ {
+ if (empty($attribute_pair['attribute_value']))
+ {
+ continue;
+ }
+
+ if ($id_attribute = $this->getAttributeId($attribute_pair))
+ {
+ $attributes_to_add[$key][] = $id_attribute;
+ if (isset($attribute_details))
+ {
+ $attributes_ids[$id_attribute] = $attribute_pair['attribute_value'];
+ }
+ }
+ }
+ }
+ $combinations = $this->generateCombinations($attributes_to_add);
+ $attribute_details_par = array('price', 'ean', 'quantity', 'weight');
+
+ if (isset($attribute_details))
+ {
+ foreach ($attribute_details_par as $parameter)
+ {
+ foreach ($combinations as $key => $combination)
+ {
+ foreach ($combination as $c)
+ {
+ if (isset($attribute_details[$parameter][$attributes_ids[$c]]))
+ {
+ $combination_detais[$key][$parameter] = $attribute_details[$parameter][$attributes_ids[$c]];
+ }
+ }
+ }
+ }
+ }
+
+ foreach ($combinations as $key => $combination)
+ {
+ $implode_sql = array();
+ if (!$combination) continue;
+ $combination_price = 0;
+
+ if (isset($combination_detais[$key]['price']))
+ {
+ $combination_price = (float)$combination_detais[$key]['price'];
+ if (!empty($this->settings['combination_price_multiplier']))
+ {
+ $combination_price *= (float)$this->settings['combination_price_multiplier'];
+ }
+
+ if (empty($this->settings['add_combination_price']))
+ {
+ $combination_price -= $product->price;
+ }
+
+ if ($combination_price < 0)
+ {
+ $combination_price = 0;
+ }
+ }
+ $combination_ean = (!empty($combination_detais[$key]['ean']) && Validate::isEan13($combination_detais[$key]['ean'])) ? $combination_detais[$key]['ean'] : '';
+
+ $id_product_attribute = $product->productAttributeExists($combination, false, null, false, true); // Last 'true' is to return id
+ if (!$id_product_attribute)
+ {
+ $combination_weight = isset($combination_detais[$key]['weight']) ? (float)$combination_detais[$key]['weight'] : 0;
+ $combination_quantity = isset($combination_detais[$key]['quantity']) ? (int)$combination_detais[$key]['quantity'] : (int)$product->quantity;
+ $id_product_attribute = $product->addCombinationEntity(
+ (float) 0,
+ $combination_price,
+ $combination_weight,
+ 0,
+ (Configuration::get('PS_USE_ECOTAX') ? (float) $product->ecotax : 0),
+ $combination_quantity,
+ null,
+ '',
+ 0,
+ $combination_ean,
+ null
+ );
+ foreach ($combination as $id_attribute)
+ {
+ Db::getInstance()->execute('
+ INSERT IGNORE INTO ' . _DB_PREFIX_ . 'product_attribute_combination (id_attribute, id_product_attribute)
+ VALUES (' . (int) $id_attribute . ',' . (int) $id_product_attribute . ')', false);
+ }
+ }
+ else
+ {
+ if (isset($attribute_details))
+ {
+ if (isset($combination_detais[$key]['price']))
+ {
+ $implode_sql['price'] = 'price = ' . (float)$combination_price;
+ }
+
+ if (isset($combination_detais[$key]['weight']))
+ {
+ $implode_sql['weight'] = 'weight = ' . (float)$combination_detais[$key]['weight'];
+ }
+
+ if (isset($combination_detais[$key]['ean']))
+ {
+ $implode_sql['ean13'] = 'ean13 = ' . pSQL($combination_ean);
+ }
+ }
+ }
+
+ if (isset($combination_detais[$key]['quantity']))
+ {
+ $implode_sql['quantity'] = 'quantity = ' . (int)$combination_detais[$key]['quantity'];
+ StockAvailable::setQuantity($id, $id_product_attribute, (int)$combination_detais[$key]['quantity']);
+ }
+
+ if ($implode_sql)
+ {
+ Db::getInstance()->execute('
+ UPDATE ' . _DB_PREFIX_ . 'product_attribute SET ' . implode(', ', $implode_sql) . ' WHERE id_product_attribute = ' . (int)$id_product_attribute);
+ unset($implode_sql['ean13']);
+ unset($implode_sql['quantity']);
+ Db::getInstance()->execute('
+ UPDATE ' . _DB_PREFIX_ . 'product_attribute_shop SET ' . implode(', ', $implode_sql) . ' WHERE id_product_attribute = ' . (int)$id_product_attribute);
+ }
+ }
+ }
+
+
+ /**
+ * copyImg copy an image located in $url and save it in a path
+ * according to $entity->$id_entity .
+ * $id_image is used if we need to add a watermark
+ *
+ * @param int $id_entity id of product or category (set in entity)
+ * @param int $id_image (default null) id of the image if watermark enabled.
+ * @param string $url path or url to use
+ * @param string $entity 'products' or 'categories'
+ * @param bool $regenerate
+ * @return bool
+ */
+ protected static function copyImg($id_entity, $id_image = null, $url, $entity = 'products', $regenerate = true)
+ {
+ $tmpfile = tempnam(_PS_TMP_IMG_DIR_, 'ps_import');
+ $watermark_types = explode(',', Configuration::get('WATERMARK_TYPES'));
+
+ switch ($entity)
+ {
+ default:
+ case 'products':
+ $image_obj = new Image($id_image);
+ $path = $image_obj->getPathForCreation();
+ break;
+ case 'categories':
+ $path = _PS_CAT_IMG_DIR_ . (int)$id_entity;
+ break;
+ case 'manufacturers':
+ $path = _PS_MANU_IMG_DIR_ . (int)$id_entity;
+ break;
+ case 'suppliers':
+ $path = _PS_SUPP_IMG_DIR_ . (int)$id_entity;
+ break;
+ }
+
+ $url = urldecode(trim($url));
+ $parced_url = parse_url($url);
+
+ if (isset($parced_url['path']))
+ {
+ $uri = ltrim($parced_url['path'], '/');
+ $parts = explode('/', $uri);
+ foreach ($parts as &$part)
+ {
+ $part = rawurlencode($part);
+ }
+ unset($part);
+ $parced_url['path'] = '/' . implode('/', $parts);
+ }
+
+ if (isset($parced_url['query']))
+ {
+ $query_parts = array();
+ parse_str($parced_url['query'], $query_parts);
+ $parced_url['query'] = http_build_query($query_parts);
+ }
+
+ if (!function_exists('http_build_url'))
+ {
+ require_once(_PS_TOOL_DIR_ . 'http_build_url/http_build_url.php');
+ }
+
+ $url = http_build_url('', $parced_url);
+
+ $orig_tmpfile = $tmpfile;
+
+ // download image by curl to avoid ssl problem
+ $curl = curl_init();
+ curl_setopt($curl, CURLOPT_URL, $url);
+ curl_setopt($curl, CURLOPT_TIMEOUT, 50);
+ curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
+ curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
+ curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
+ $image = curl_exec($curl);
+ curl_close($curl);
+
+ // copy image to $tmpfile
+ file_put_contents($tmpfile, $image);
+
+ // if $tmpfile is not readable, return false
+ if (!filesize($tmpfile))
+ {
+ @unlink($tmpfile);
+ return false;
+ }
+
+ // Evaluate the memory required to resize the image: if it's too much, you can't resize it.
+ if (!ImageManager::checkImageMemoryLimit($tmpfile))
+ {
+ @unlink($tmpfile);
+ return false;
+ }
+
+ // Sprawdź typ MIME pliku
+ $finfo = finfo_open(FILEINFO_MIME_TYPE);
+ $mime_type = finfo_file($finfo, $tmpfile);
+ finfo_close($finfo);
+
+ if ($mime_type == 'image/webp')
+ {
+ // Wczytaj obraz WebP
+ $image = imagecreatefromwebp($tmpfile);
+ if ($image === false)
+ {
+ @unlink($tmpfile);
+ return false;
+ }
+ // Zapisz obraz jako JPG, nadpisując oryginalny plik
+ imagejpeg($image, $tmpfile, 100); // 100% jakości
+ imagedestroy($image);
+ }
+
+ if (!ImageManager::isRealImage($tmpfile))
+ {
+ @unlink($tmpfile);
+ return false;
+ }
+
+ $tgt_width = $tgt_height = 0;
+ $src_width = $src_height = 0;
+ $error = 0;
+ ImageManager::resize(
+ $tmpfile,
+ $path . '.jpg',
+ null,
+ null,
+ 'jpg',
+ false,
+ $error,
+ $tgt_width,
+ $tgt_height,
+ 5,
+ $src_width,
+ $src_height
+ );
+ $images_types = ImageType::getImagesTypes($entity, true);
+
+ if ($regenerate)
+ {
+ $previous_path = null;
+ $path_infos = array();
+ $path_infos[] = array($tgt_width, $tgt_height, $path . '.jpg');
+ foreach ($images_types as $image_type)
+ {
+ $tmpfile = self::get_best_path($image_type['width'], $image_type['height'], $path_infos);
+
+ if (ImageManager::resize(
+ $tmpfile,
+ $path . '-' . stripslashes($image_type['name']) . '.jpg',
+ $image_type['width'],
+ $image_type['height'],
+ 'jpg',
+ false,
+ $error,
+ $tgt_width,
+ $tgt_height,
+ 5,
+ $src_width,
+ $src_height
+ ))
+ {
+ // the last image should not be added in the candidate list if it's bigger than the original image
+ if ($tgt_width <= $src_width && $tgt_height <= $src_height)
+ {
+ $path_infos[] = array($tgt_width, $tgt_height, $path . '-' . stripslashes($image_type['name']) . '.jpg');
+ }
+ if ($entity == 'products')
+ {
+ if (is_file(_PS_TMP_IMG_DIR_ . 'product_mini_' . (int)$id_entity . '.jpg'))
+ {
+ unlink(_PS_TMP_IMG_DIR_ . 'product_mini_' . (int)$id_entity . '.jpg');
+ }
+ if (is_file(_PS_TMP_IMG_DIR_ . 'product_mini_' . (int)$id_entity . '_' . (int)Context::getContext()->shop->id . '.jpg'))
+ {
+ unlink(_PS_TMP_IMG_DIR_ . 'product_mini_' . (int)$id_entity . '_' . (int)Context::getContext()->shop->id . '.jpg');
+ }
+ }
+ }
+ if (in_array($image_type['id_image_type'], $watermark_types))
+ {
+ Hook::exec('actionWatermark', array('id_image' => $id_image, 'id_product' => $id_entity));
+ }
+ }
+ }
+
+ unlink($orig_tmpfile);
+ return true;
+ }
+
+ private static function get_best_path($tgt_width, $tgt_height, $path_infos)
+ {
+ $path_infos = array_reverse($path_infos);
+ $path = '';
+ foreach ($path_infos as $path_info)
+ {
+ list($width, $height, $path) = $path_info;
+ if ($width >= $tgt_width && $height >= $tgt_height)
+ {
+ return $path;
+ }
+ }
+ return $path;
+ }
+
+ public function getProductId($query, $table, $identifier = 'name')
+ {
+ $sql = new DbQuery();
+ $sql->select('p.`id_product`, p.`ean13`, p.`upc`, p.`active`, p.`reference`');
+ $sql->from('product', 'p');
+
+ if ($identifier == 'name')
+ {
+ $sql->join(Shop::addSqlAssociation('product', 'p'));
+ $sql->leftJoin(
+ 'product_lang',
+ 'pl',
+ '
+ p.`id_product` = pl.`id_product`
+ AND pl.`id_lang` = ' . (int)$this->id_lang . Shop::addSqlRestrictionOnLang('pl')
+ );
+ $where = 'pl.`name` = \'' . pSQL($query) . '\'';
+ }
+ else
+ {
+ $where = 'p.`' . $identifier . '` = \'' . pSQL($query) . '\'';
+ }
+
+ $sql->where($where);
+
+ $sql->limit(1);
+
+ $result = Db::getInstance()->executeS($sql);
+
+ if (!empty($result[0]['id_product']))
+ {
+ return $result[0]['id_product'];
+ }
+
+ return 0;
+ }
+
+ public function editProduct($product_id, $original_product)
+ {
+ $simple_fields = array('price', 'wholesale_price', 'additional_shipping_cost', 'ean13', 'ups', 'condition', 'width', 'height', 'depth', 'weight', 'active', 'minimal_quantity');
+ $product = new Product($product_id);
+
+ if ($product->name == null)
+ {
+ return $product;
+ }
+
+ if (!empty($this->settings['update_manufacturer']))
+ {
+ if (!empty($original_product['brand']))
+ {
+ $product->id_manufacturer = $this->getManufacturerId($original_product['brand']);
+ }
+ else
+ {
+ $product->id_manufacturer = $this->settings['default_manufacturer_id'];
+ }
+ }
+
+ foreach ($simple_fields as $field)
+ {
+ if (!empty($this->settings['update_' . $field]) && isset($original_product[$field]))
+ {
+ $product->$field = $original_product[$field];
+ }
+ }
+ if (isset($this->settings['update_id_tax_rules_group']) && $this->settings['id_tax_rules_group'] != -1)
+ {
+ $product->id_tax_rules_group = $this->settings['id_tax_rules_group'];
+ }
+
+ foreach (Language::getLanguages(false) as $lang)
+ {
+ if (!empty($this->settings['update_name']))
+ {
+ $original_product['name'] = htmlspecialchars_decode($original_product['name']);
+ $original_product['name'] = str_replace(['>', '<', '=', ';', '{', '}', '#'], ' ', $original_product['name']);
+ $product->name[$lang['id_lang']] = $original_product['name'];
+ $product->link_rewrite[$lang['id_lang']] = Tools::link_rewrite($original_product['name']);
+ $product->meta_keywords[$lang['id_lang']] = str_replace(' ', ',', $original_product['name']);
+ }
+
+ if (!empty($this->settings['update_description']))
+ {
+ $original_product['description'] = nl2br($original_product['description']);
+ $original_product['description'] = htmlspecialchars_decode(htmlspecialchars_decode($original_product['description'], ENT_COMPAT));
+ $product->description_long[$lang['id_lang']] = $original_product['description'];
+ $product->description[$lang['id_lang']] = $original_product['description'];
+ }
+
+ if (!empty($original_product['short_description']) && !empty($this->settings['update_short_description']))
+ {
+ $product->description_short[$lang['id_lang']] = $original_product['short_description'];
+ }
+ }
+
+ $product->additional_delivery_times = 2;
+ $product->delivery_in_stock[7] = '1-5 dni roboczych';
+
+ //$product->active = isset($original_product['active']) ? (int)$original_product['active'] : 1;;
+
+ $product->save();
+
+ if (!empty($this->settings['update_quantity']))
+ {
+ StockAvailable::setQuantity((int)$product_id, 0, $original_product['quantity']);
+ }
+
+ // check if product has no images
+ $images_count = "SELECT COUNT(0) FROM " . _DB_PREFIX_ . "image WHERE id_product = " . $product->id;
+ $result_images_count = Db::getInstance()->getValue($images_count);
+
+ if (!empty($this->settings['update_images']) and $result_images_count == 0)
+ {
+ foreach ($original_product['images'] as $image_url)
+ {
+ $this->addImageToProduct($image_url, $product->id);
+ }
+
+ if ($original_product['cover'])
+ {
+ $this->addImageToProduct($original_product['cover'], $product->id, true);
+ }
+ }
+
+ if (!empty($this->settings['update_cover']) && !empty($original_product['cover']))
+ {
+ $this->addImageToProduct($original_product['cover'], $product->id, true);
+ }
+
+ if ($original_product['feature'] and $original_product['feature_value'])
+ {
+ $original_product['features'] = array(
+ array(
+ 'feature' => $original_product['feature'],
+ 'feature_value' => $original_product['feature_value']
+ )
+ );
+ }
+
+ if (!empty($this->settings['update_features']) && !empty($original_product['features']))
+ {
+ $this->addFeaturesToProduct($original_product['features'], $product->id);
+ }
+
+ if (!empty($this->settings['update_attributes']) && !empty($original_product['attributes']))
+ {
+ $attribute_details = !empty($original_product['attribute_details']) ? $original_product['attribute_details'] : array();
+ $this->addAttributesToProduct($original_product['attributes'], $product->id, $attribute_details);
+ }
+
+ if ( $this -> settings['update_active'] )
+ {
+ $sql = 'UPDATE ' . _DB_PREFIX_ . 'product SET active = ' . (int)$original_product['active'] . ' WHERE id_product = ' . (int)$product->id;
+ Db::getInstance()->execute($sql);
+ // materac_product_shop
+ $sql_shop = 'UPDATE ' . _DB_PREFIX_ . 'product_shop SET active = ' . (int)$original_product['active'] . ' WHERE id_product = ' . (int)$product->id;
+ Db::getInstance()->execute($sql_shop);
+ }
+
+ if ( $this -> settings['update_quantity'] )
+ {
+ $sql = 'UPDATE ' . _DB_PREFIX_ . 'stock_available SET quantity = ' . (int)$original_product['quantity'] . ' WHERE id_product = ' . (int)$product->id;
+ Db::getInstance()->execute($sql);
+ }
+
+ return $product;
+ }
+
+ public function processMissing($file_id, $queue_id)
+ {
+ $sql = "SELECT * FROM " . _DB_PREFIX_ . "ia_products WHERE shop = 'default' AND file_id = " . (int)$file_id . " AND queue_id != " . (int)$queue_id;
+
+ //$sql .= " LIMIT 20";
+
+ if (!empty($this->settings['not_existing']) && $products = Db::getInstance()->executeS($sql))
+ {
+
+ if ($this->settings['not_existing'] == 1)
+ {
+ foreach ($products as $product)
+ {
+ StockAvailable::setQuantity((int)$product['product_id'], 0, 0);
+ }
+ }
+ elseif ($this->settings['not_existing'] == 2)
+ {
+ foreach ($products as $product)
+ {
+ $productObject = new Product($product['product_id']);
+ if ($productObject->name)
+ {
+ $productObject->active = 0;
+ $productObject->save();
+ }
+ }
+ }
+ elseif ($this->settings['not_existing'] == 3)
+ {
+ foreach ($products as $product)
+ {
+ $productObject = new Product($product['product_id']);
+ if ($productObject->name)
+ {
+ $productObject->delete();
+ }
+ Db::getInstance()->execute("DELETE FROM " . _DB_PREFIX_ . "ia_products WHERE product_id = '" . (int)$product['product_id'] . "'");
+ }
+ }
+ }
+ Db::getInstance()->execute("UPDATE " . _DB_PREFIX_ . "ia_queues SET source = 'admin', status = '4', date_processed = '" . time() . "' WHERE queue_id = '" . (int)$queue_id . "'");
+ }
+
+ public function setAttributes()
+ {
+ $default_language = $this->id_lang;
+
+ $this->groups = array();
+
+ foreach (AttributeGroup::getAttributesGroups($default_language) as $group)
+ {
+ $this->groups[$group['name']] = (int) $group['id_attribute_group'];
+ }
+
+ $this->attributes = array();
+
+ foreach (Attribute::getAttributes($default_language) as $attribute)
+ {
+ $this->attributes[$attribute['attribute_group'] . '_' . $attribute['name']] = (int) $attribute['id_attribute'];
+ }
+ }
+
+ function getAttributeGroupId($name)
+ {
+ $tab_group = explode(':', $name);
+ $group = trim($tab_group[0]);
+ if (isset($this->groups[$group]))
+ {
+ return $this->groups[$group];
+ }
+ if (!isset($tab_group[1]))
+ {
+ $type = 'select';
+ }
+ else
+ {
+ $type = trim($tab_group[1]);
+ }
+ $obj = new AttributeGroup();
+ $obj->is_color_group = false;
+ $obj->group_type = pSQL($type);
+ $obj->name[$this->id_lang] = $group;
+ $obj->public_name[$this->id_lang] = $group;
+ //$obj->position = (!$position) ? AttributeGroup::getHigherPosition() + 1 : $position;
+ $obj->add();
+ $this->groups[$group] = $obj->id;
+ return $obj->id;
+ }
+
+ function getAttributeId($pair)
+ {
+ $name = $pair['attribute_value'];
+ $group = $pair['attribute'];
+
+ if (isset($this->attributes[$group . '_' . $name]))
+ {
+ return $this->attributes[$group . '_' . $name];
+ }
+
+ $id_attribute_group = $this->getAttributeGroupId($group);
+
+ $obj = new Attribute();
+ $obj->id_attribute_group = $id_attribute_group;
+ $obj->name[$this->id_lang] = str_replace('\n', '', str_replace('\r', '', $name));
+ $obj->add();
+ $this->attributes[$group . '_' . $name] = $obj->id;
+ return $obj->id;
+ }
+
+ function generateCombinations($attributes_to_add)
+ {
+ $i = 0;
+ $combinations = array();
+ // 3 options
+ if (count($attributes_to_add) == 3)
+ {
+ foreach ($attributes_to_add[1] as $attr_1)
+ {
+ $temp_1 = isset($combinations[$i]) ? $combinations[$i] : array();
+ $combinations[$i][] = $attr_1;
+ foreach ($attributes_to_add[2] as $attr_2)
+ {
+ $temp_2 = $combinations[$i];
+ $combinations[$i][] = $attr_2;
+ foreach ($attributes_to_add[3] as $attr_3)
+ {
+ $temp_3 = $combinations[$i];
+ $combinations[$i][] = $attr_3;
+ $i++;
+ $combinations[$i] = $temp_3;
+ }
+
+ $combinations[$i] = $temp_2;
+ }
+ $combinations[$i] = $temp_1;
+ }
+ }
+ elseif (count($attributes_to_add) == 2 && isset($attributes_to_add[1]))
+ {
+ foreach ($attributes_to_add[1] as $attr_1)
+ {
+ $temp_1 = isset($combinations[$i]) ? $combinations[$i] : array();
+ $combinations[$i][] = $attr_1;
+ foreach ($attributes_to_add[2] as $attr_2)
+ {
+ $temp_2 = $combinations[$i];
+ $combinations[$i][] = $attr_2;
+ $i++;
+ $combinations[$i] = $temp_2;
+ }
+
+ $combinations[$i] = $temp_1;
+ }
+ }
+ elseif (count($attributes_to_add) == 1 && isset($attributes_to_add[1]))
+ {
+ foreach ($attributes_to_add[1] as $attr_1)
+ {
+ //$temp_0 = isset($combinations[$i]) ? $combinations[$i] : array();
+ $combinations[$i][] = $attr_1;
+
+ $i++;
+ //$combinations[$i] = $temp_0;
+ }
+ }
+ return $combinations;
+ }
+}
diff --git a/modules/import_api/classes/filereader.php b/modules/import_api/classes/filereader.php
new file mode 100644
index 00000000..0baedac6
--- /dev/null
+++ b/modules/import_api/classes/filereader.php
@@ -0,0 +1,425 @@
+
+ * @copyright 2019 Dalibor Stojcevski
+ * @license Dalibor Stojcevski
+ */
+ini_set('memory_limit', '-1');
+ini_set('display_startup_errors', 1);
+ini_set('display_errors', 1);
+error_reporting(E_ALL ^ E_DEPRECATED);
+
+/**
+ * Adds the depreciated each() function back into 7.2
+ */
+if (!function_exists('each')) {
+ function each($arr) {
+ $key = key($arr);
+ $result = ($key === null) ? false : [$key, current($arr), 'key' => $key, 'value' => current($arr)];
+ next($arr);
+ return $result;
+ }
+}
+if (!function_exists('array_key_first')) {
+ function array_key_first(array $arr) {
+ foreach($arr as $key => $unused) {
+ return $key;
+ }
+ return NULL;
+ }
+}
+class FileReader
+{
+ public $types = array(
+ 'csv' => array(
+ 'application/octet-stream',
+ 'text/csv',
+ 'application/csv',
+ 'text/comma-separated-values',
+ 'application/excel',
+ 'application/vnd.ms-excel',
+ 'application/vnd.msexcel',
+ ),
+ 'json' => array(
+ 'application/json',
+ 'text/json',
+ 'application/x-javascript',
+ 'application/javascript',
+ 'text/javascript',
+ 'text/x-javascript',
+ 'text/x-json',
+ ),
+ 'xml' => array(
+ 'application/xml',
+ 'text/xml',
+ )
+ );
+
+ public $mime = '';
+
+ public function getFile($link, $post = '', $curl = true) {
+
+ $link = str_replace('amp;', '', $link);
+ //$link = urlencode($link);
+ $link = str_replace(" ", "%20", $link); // to properly format the url
+ $link_parts = parse_url($link);
+ if (!empty($link_parts['user']) && !empty($link_parts['pass'])) {
+ $link = str_replace($link_parts['user'] . ':' . $link_parts['pass'] . '@', '', $link);
+ }
+
+ if($curl) {
+ $error_msg = '';
+
+ $ch = curl_init();
+
+ curl_setopt($ch, CURLOPT_URL, $link);
+
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+
+ curl_setopt($ch, CURLOPT_FAILONERROR, true); // Required for HTTP error codes to be reported via our call to curl_error($ch)
+
+ curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
+
+ curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); // This is very important if url have download
+
+ if ($post) {
+ curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
+ curl_setopt($ch, CURLOPT_HTTPHEADER, array(
+ 'Content-Type: application/xml',
+ 'Content-Length: ' . strlen($post))
+ );
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
+
+ }
+
+ if (!empty($link_parts['user']) && !empty($link_parts['pass'])) {
+ curl_setopt($ch, CURLOPT_USERPWD, $link_parts['user'] . ":" . $link_parts['pass']);
+ }
+
+
+ $server_output = curl_exec($ch);
+
+ $mime_parts = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
+
+ if ($mime_parts) {
+ $mime = explode(';', $mime_parts);
+ $this->mime = isset($mime[0]) ? $mime[0] : '';
+ }
+
+ if (curl_errno($ch)) {
+ $error_msg = curl_error($ch);
+ //print_r('--'. $error_msg .'-');
+ //var_dump($link);
+ //exit;
+ }
+ curl_close($ch);
+ unset($ch);
+
+ return array($server_output, $error_msg);
+
+ } else {
+ if (!empty($link_parts['user']) && !empty($link_parts['pass'])) {
+ $auth = base64_encode($link_parts['user'] . ":" . $link_parts['pass']);
+ $context = stream_context_create([
+ "http" => [
+ "header" => "Authorization: Basic $auth"
+ ]
+ ]);
+ $server_output = @file_get_contents($link, false, $context);
+ } else {
+ $server_output = @file_get_contents($link);
+ }
+ return array($server_output,'');
+ }
+
+ return false;
+ }
+
+ public function getArrayFromLink($file) {
+
+ $substring = '';
+
+ $php_array = array();
+
+ $link = trim($file['link']);
+ $source = trim($file['source']);
+ $post = trim($file['post']);
+ $delimiter = !empty($file['delimiter']) ? $file['delimiter'] : '';
+ $headers = !empty($file['headers']) ? $file['headers'] : 0;
+
+ $session_file_name = 'tmp/files/' . $file['shop'] .'/session_' . session_id() . '_' . $file['file_id']. '.txt';
+
+ $file_ext = pathinfo($link, PATHINFO_EXTENSION);
+ if (in_array($file_ext, ['xlsx', 'xls'])) {
+ $source = 'excel';
+ }
+ if ($source == 'excel') {
+ $error = '';
+ $external_string = '';
+
+ } else {
+ list($external_string, $error) = $result = $this->getFile($link, $post);
+ if (empty($external_string)) {
+ list($external_string, $error) = $result = $this->getFile($link, $post, false);
+ }
+ if (!$error && $external_string) {
+ //file_put_contents($session_file_name, $external_string);
+ }
+ }
+
+ if ($error) {
+ return array('', $error);
+ }
+
+ $mime = $file['mime_type'] ? trim($file['mime_type']) : $this->mime;
+
+ if ($external_string === false){
+ $error = 'External file not found. Check link in browser';
+ return array('', $error);
+ }
+
+ $external_string = $this->removeBOM($external_string);
+ $external_string = trim($external_string);
+
+ if (!$source) {
+
+ $substring = $external_string ? substr($external_string, 0, 200) : ' ';
+
+
+ $substring = preg_replace('/[\x00-\x1F\x80-\xFF]/', '', $substring);
+ if (!$substring) {
+ $substring = ' ';
+ }
+ if (in_array($substring[0],['<'])) {
+ $source = 'xml';
+ } elseif (in_array($substring[0],['[', '{'])) {
+ $source = 'json';
+ } elseif ($mime) {
+ $source = $this->matchMimeType($mime);
+ }
+ }
+
+
+ if ($source == 'json') {
+ $json_string = $external_string;
+ $php_array = json_decode($json_string, true);
+ }
+
+ if ($source == 'xml') {
+ libxml_use_internal_errors(true);
+ $ob = simplexml_load_string($external_string, 'SimpleXMLElement', LIBXML_NOCDATA | LIBXML_PARSEHUGE);
+ if ($ob) {
+ $php_array = $this->xmlToArray($ob);
+ } else {
+ //echo libxml_get_errors();
+ return array($php_array, libxml_get_errors());
+
+ }
+ }
+
+ ini_set('auto_detect_line_endings',TRUE);
+
+ if ($source == 'excel') {
+ $parts = explode('/modules/import_api/views/img/', $link);
+ if (isset($parts[1])) {
+ $file_path = 'modules/importer_api/views/img/' . $parts[1];
+ } else {
+ return array([], 'File not found ( excel error )');
+ }
+
+ if ($file_ext == 'xlsx') {
+ require_once(_PS_MODULE_DIR_ . 'import_api/lib/SimpleXLSX.php');
+ $xls = new SimpleXLSX($file_path);
+ } elseif ($file_ext == 'xls') {
+ require_once(_PS_MODULE_DIR_ . 'import_api/lib/SimpleXLS.php');
+ $xls = new SimpleXLS($file_path);
+ }
+
+ if ($xls->success()) {
+ foreach($xls->rows() as $row){
+ if (empty($index)) {
+ if ($headers) {
+ $c = 1;
+ foreach ($row as $r) {
+ $index[] = 'COLUMN ' . $c;
+ $c++;
+ }
+
+ } else {
+ $index = $row;
+ }
+
+ } else {
+ $php_array[] = @array_combine($index, $row);
+ }
+
+ }
+ } else {
+ return array([], 'Error reading Excel file. ' . $xls->error());
+ }
+ }
+
+
+ if (!$php_array || $source == 'csv') {
+
+ $fp = tmpfile();
+ fwrite($fp, $external_string);
+ rewind($fp); //rewind to process CSV
+
+ if (!$delimiter) {
+ $delimiter = $this->detectDelimiter($fp);
+ }
+
+ while (($row = fgetcsv($fp, 0, $delimiter)) !== FALSE) {
+ if (empty($index)) {
+ if ($headers) {
+ $c = 1;
+ foreach ($row as $r) {
+ $index[] = 'COLUMN ' . $c;
+ $c++;
+ }
+
+ } else {
+ $index = $row;
+ }
+
+ } else {
+ $php_array[] = @array_combine($index, $row);
+ }
+ }
+ /*
+ $external_array = explode("\n", $external_string);
+ $index = str_getcsv(array_shift($external_array)); // not work with new lines
+ $php_array = null;
+ foreach ($external_array as $e) {
+ if ($e)
+ $php_array[] = @array_combine($index, $row);
+ }
+ */
+ }
+
+ if (!$php_array && $file['source']) {
+ $file['source'] = '';
+ return $this->getArrayFromLink($file);
+ }
+
+
+ return array($php_array, '');
+ }
+
+ public function xmlToArray($xml, $options = array()) {
+ $defaults = array(
+ 'namespaceSeparator' => ':',//you may want this to be something other than a colon
+ 'attributePrefix' => '@', //to distinguish between attributes and nodes with the same name
+ 'alwaysArray' => array(), //array of xml tag names which should always become arrays
+ 'autoArray' => true, //only create arrays for tags which appear more than once
+ 'textContent' => 'VAL', //key used for the text content of elements
+ 'autoText' => true, //skip textContent key if node has no attributes or child nodes
+ 'keySearch' => false, //optional search and replace on tag and attribute names
+ 'keyReplace' => false //replace values for above search values (as passed to str_replace())
+ );
+ $options = array_merge($defaults, $options);
+ $namespaces = $xml->getDocNamespaces();
+ $namespaces[''] = null; //add base (empty) namespace
+
+ //get attributes from all namespaces
+ $attributesArray = array();
+ foreach ($namespaces as $prefix => $namespace) {
+ foreach ($xml->attributes($namespace) as $attributeName => $attribute) {
+ //replace characters in attribute name
+ if ($options['keySearch']) $attributeName =
+ str_replace($options['keySearch'], $options['keyReplace'], $attributeName);
+ $attributeKey = $options['attributePrefix']
+ . ($prefix ? $prefix . $options['namespaceSeparator'] : '')
+ . $attributeName;
+ $attributesArray[$attributeKey] = (string)$attribute;
+ }
+ }
+
+ //get child nodes from all namespaces
+ $tagsArray = array();
+ foreach ($namespaces as $prefix => $namespace) {
+ foreach ($xml->children($namespace) as $childXml) {
+ //recurse into child nodes
+ $childArray = $this->xmlToArray($childXml, $options);
+ list($childTagName, $childProperties) = each($childArray);
+
+ //replace characters in tag name
+ if ($options['keySearch']) $childTagName =
+ str_replace($options['keySearch'], $options['keyReplace'], $childTagName);
+ //add namespace prefix, if any
+ if ($prefix) $childTagName = $prefix . $options['namespaceSeparator'] . $childTagName;
+
+ if (!isset($tagsArray[$childTagName])) {
+ //only entry with this key
+ //test if tags of this type should always be arrays, no matter the element count
+ $tagsArray[$childTagName] =
+ in_array($childTagName, $options['alwaysArray']) || !$options['autoArray']
+ ? array($childProperties) : $childProperties;
+ } elseif (
+ is_array($tagsArray[$childTagName]) && array_keys($tagsArray[$childTagName])
+ === range(0, count($tagsArray[$childTagName]) - 1)
+ ) {
+ //key already exists and is integer indexed array
+ $tagsArray[$childTagName][] = $childProperties;
+ } else {
+ //key exists so convert to integer indexed array with previous value in position 0
+ $tagsArray[$childTagName] = array($tagsArray[$childTagName], $childProperties);
+ }
+ }
+ }
+
+ //get text content of node
+ $textContentArray = array();
+ $plainText = trim((string)$xml);
+ if ($plainText !== '') $textContentArray[$options['textContent']] = $plainText;
+
+ //stick it all together
+ $propertiesArray = !$options['autoText'] || $attributesArray || $tagsArray || ($plainText === '')
+ ? array_merge($attributesArray, $tagsArray, $textContentArray) : $plainText;
+
+ //return node as array
+ return array(
+ $xml->getName() => $propertiesArray
+ );
+ }
+
+ public function matchMimeType($mime) {
+
+ foreach ($this->types as $type => $list) {
+ if (in_array($mime, $list)) {
+ return $type;
+ }
+ }
+ return '';
+ }
+
+ public function detectDelimiter($fh) {
+ $delimiters = [";","\t", "|", ","];
+ $data_1 = null; $data_2 = array();
+ $delimiter = $delimiters[0];
+ foreach($delimiters as $d) {
+ $data_1 = fgetcsv($fh, 4096, $d);
+ if(sizeof($data_1) > sizeof($data_2)) {
+ $delimiter = $d;
+ $data_2 = $data_1;
+ }
+ rewind($fh);
+ }
+
+ return $delimiter;
+ }
+
+ function removeBOM($data) {
+ if (0 === strpos(bin2hex($data), 'efbbbf')) {
+ return substr($data, 3);
+ }
+ return $data;
+ }
+}
\ No newline at end of file
diff --git a/modules/import_api/classes/import.php b/modules/import_api/classes/import.php
new file mode 100644
index 00000000..e2037312
--- /dev/null
+++ b/modules/import_api/classes/import.php
@@ -0,0 +1,554 @@
+
+ * @copyright 2019 Dalibor Stojcevski
+ * @license Dalibor Stojcevski
+ */
+
+/** No max line limit since the lines can be more than 4096. Performance impact is not significant. */
+
+function filter_category_array ($element) {
+ return (!is_array($element) && $element !== '');
+}
+
+define('MAX_LINE_SIZE', 0);
+
+class Import
+{
+
+ private $json_array = array();
+ private $product_links = array();
+ private $settings;
+
+ public function __construct($settings)
+ {
+ $this->settings = $settings;
+ }
+
+ public function getAllData($path, $test = false)
+ {
+ $fields = $this->settings['import_api_field'];
+ $nested_data = array();
+
+ $id_parts = explode('->', $path);
+
+ array_shift($id_parts);
+
+ foreach ($fields as $field => $value) {
+ if(!$value) continue;
+ $field_paths = $value ? html_entity_decode($value, ENT_QUOTES, 'UTF-8') : '';
+ $field_parts = explode('->', $field_paths);
+
+ $nested_data[$field] = $this->getFieldData($this->json_array, $field_parts, $id_parts);
+ }
+
+ if ($test == 'view-raw') {
+ return $nested_data;
+ }
+
+ $nested_data = $this->checkCombinations($nested_data);
+
+ if ($test == 'view-split') {
+ return $nested_data;
+ }
+
+ $original_product = $this->resolveAllNestedData($nested_data);
+
+ if ($test == 'view-grouping') {
+ return $original_product;
+ }
+
+ $modified = $this->changeOriginal($original_product);
+
+ return $modified;
+ }
+
+ public function resolveAllNestedData($nested_data)
+ {
+ if (!empty($nested_data['feature_value'])) {
+
+ if (empty($nested_data['feature'])) {
+ if (!empty($this->settings['import_api_modification']['feature'])) {
+ $nested_data['feature'] = $this->settings['import_api_modification']['feature'];
+ } else {
+ $nested_data['feature'] = 'Feature';
+ }
+ }
+
+ $feature_map = $this->resolveNested($nested_data['feature_value'], $nested_data['feature']);
+ $nested_data['features'] = array();
+
+ foreach ($feature_map as $key => $map) {
+ $nested_data['features'][$key] = array(
+ 'feature' => $map['parent'],
+ 'feature_value' => $map['value']
+ );
+ }
+ }
+$nested_data['attributes'] = array();
+for ($i = 1;$i <= 3; $i++) {
+
+ if (!empty($nested_data['attribute_value' . $i])) {
+ if (empty($nested_data['attribute' . $i])) {
+ if (!empty($this->settings['import_api_modification']['attribute' . $i])) {
+ $nested_data['attribute' . $i] = $this->settings['import_api_modification']['attribute' . $i];
+ } else {
+ $nested_data['attribute' . $i] = 'Select ' . $i;
+ }
+ }
+
+ $attribute_map = $this->resolveNested($nested_data['attribute_value' . $i], $nested_data['attribute' . $i]);
+
+ $attribute_details_par = array('price', 'ean', 'quantity', 'weight');
+ foreach ($attribute_map as $key => $map) {
+ foreach ($attribute_details_par as $parameter) {
+ if (isset($nested_data['attribute_' . $parameter])) {
+ if (is_array($nested_data['attribute_' . $parameter])) {
+ if (isset($nested_data['attribute_' . $parameter][$key])) {
+ $nested_data['attribute_details'][$parameter][$map['value']] = $nested_data['attribute_' . $parameter][$key];
+ }
+ } else {
+ $nested_data['attribute_details'][$parameter][$map['value']] = $nested_data['attribute_' . $parameter];
+ }
+ }
+ }
+ $nested_data['attributes'][$i][$key] = array(
+ 'attribute' => $map['parent'],
+ 'attribute_value' => $map['value']
+ );
+ }
+ }
+}
+
+ if (!empty($nested_data['category'])) {
+
+ if (!isset($nested_data['category_parent'])) {
+ $nested_data['category_parent'] = 0; // it is not good idea to put default top category here
+ }
+
+ if ($this->settings['import_api_settings']['category_path'] && is_array($nested_data['category'])) {
+
+ $nested_data['category'] = implode('->', array_filter($nested_data['category'], "filter_category_array"));
+ }
+
+ $nested_data['category_path'] = $this->resolveNested($nested_data['category'], $nested_data['category_parent']);
+ }
+
+ $nested_data['options'] = array();
+ if (!empty($nested_data['option_value'])) {
+
+ if (!isset($nested_data['option'])) {
+ $nested_data['option'] = $this->settings['import_api_default_option'];
+ }
+
+ if (!isset($nested_data['option_price'])) {
+ $nested_data['option_price'] = 0;
+ }
+
+ if (!isset($nested_data['option_weight'])) {
+ $nested_data['option_weight'] = 0;
+ }
+
+ $option_map= $this->resolveNested($nested_data['option_value'], $nested_data['option']);
+ $option_price_map = $this->resolveNested($nested_data['option_price'], $nested_data['option_value']);
+ $option_weight_map = $this->resolveNested($nested_data['option_weight'], $nested_data['option_value']);
+ foreach ($option_map as $key => $map) {
+ if (is_array($nested_data['option_price'])) {
+ foreach ($option_price_map as $price_map) {
+ if ($map['value'] == $price_map['parent']) {
+ $nested_data['options'][$key] = array(
+ 'option' => $map['parent'],
+ 'option_value' => $map['value'],
+ 'price' => $price_map['value']
+ );
+ break;
+ }
+ }
+ } else {
+ $nested_data['options'][$key] = array(
+ 'option' => $map['parent'],
+ 'option_value' => $map['value'],
+ 'price' => $nested_data['option_price']
+ );
+ }
+
+ if (is_array($nested_data['option_weight'])) {
+ foreach ($option_weight_map as $weight_map) {
+ if($map['value'] == $weight_map['parent']){
+ $nested_data['options'][$key]['weight'] = $weight_map['value'];
+ break;
+ }
+ }
+ } else {
+ $nested_data['options'][$key]['weight'] = $nested_data['option_weight'];
+ }
+ }
+
+ }
+
+ return $nested_data;
+ }
+ public function getFieldData($tree, $field_parts, $id_parts)
+ {
+ while($field_parts) {
+ if ($this->isSeq($tree)) {
+ $seq_id = array_shift($id_parts);
+ $tree = $tree[$seq_id];
+ } else {
+
+ $part = array_shift($field_parts);
+ $uni_c = array_shift($id_parts);
+
+ if($part == $uni_c){
+ $tree = $tree[$part];
+
+ } else {
+ $field_parts = array_merge([$part], $field_parts); // vrati mu ga
+ return $this->followDifferentPath($field_parts, $tree);
+ }
+ }
+ }
+ }
+
+ public function followDifferentPath($own_parts, $tree){
+ $prev= ''; // for test
+ foreach($own_parts as $part){
+//if ($part == 'image') {var_dump($tree);var_dump($own_parts);exit;}
+
+ if ($this->isSeq($tree)) {
+ $ret = array();
+ foreach($tree as $t){
+ $ret[] = $this->followDifferentPath($own_parts, $t);
+ }
+ return $ret;
+ } else {
+ array_shift($own_parts);
+ $tree = isset($tree[$part]) ? $tree[$part] : '';
+ }
+ $prev = $part; //for test
+ }
+
+ if(!is_array($tree)){
+ return trim($tree);
+ }
+
+ return $tree;
+ }
+
+ public function findUniqueProductsIdentifier($tree, $parts, $identifier, $path = ''){
+
+ $tmp_parts = $parts;
+ foreach ($parts as $part) {
+ if ($part == $identifier && !$this->isSeq($tree) && isset($tree[$part])) {
+
+ if (!empty($tree[$part]) && !is_array($tree[$part])) {
+ $path .= '->'. $tree[$part];
+ $this->product_links[] = $path;
+ }
+
+ //exit;
+
+ } elseif ($this->isSeq($tree)) {
+
+ foreach($tree as $key => $t){
+ $local_path = $path. '->' . $key;
+ $this->findUniqueProductsIdentifier($t, $tmp_parts, $identifier, $local_path);
+ }
+ return;
+ } else {
+
+ if (isset($tree[$part])) {
+ $tree = $tree[$part];
+ $path .= '->'. $part;
+ }
+
+ array_shift($tmp_parts);
+ }
+ }
+ }
+
+ public function resolveNested($child_data, $parent_data, $default_parent = 'Value'){
+
+ $child_indexes = array();
+
+ if(!is_array($child_data)){
+ $child_data = array($child_data);
+ }
+
+ $this->setIndexPath($child_data, $child_indexes);
+
+
+ $parent_indexes = array();
+
+ if(is_array($parent_data)){
+ $this->setIndexPath($parent_data, $parent_indexes);
+ } else {
+ $map = array();
+ foreach($child_indexes as $child){
+
+ $map[] = array(
+ 'value' => $child['value'],
+ 'parent' => $parent_data
+ );
+ }
+
+ return $map;
+ }
+
+ return $this->addParent($child_indexes, $parent_indexes);
+
+ }
+
+ public function addParent($children, $parents){
+ $map = array();
+ foreach($parents as $parent){
+ foreach($children as $key => $child){
+ if(substr($child['key_path'], 0, strlen($parent['key_path'])) === $parent['key_path']){
+ $map[] = array(
+ 'value' => $child['value'],
+ 'parent' => $parent['value']
+ );
+
+ unset($children[$key]);
+ }
+ }
+ }
+
+ return $map;
+ }
+
+ /**
+ * Takes nested array with unknown nesting levels and return one level array with indexes to values.
+ *
+ * @param array $array array with unknown nesting levels
+ * @param string $key_path string made from array keys
+ * @return array with values and string from value indexes
+ */
+
+ public function setIndexPath($array, &$return_values = array(), $key_path = ''){
+ foreach($array as $key => $array_part){
+
+ if(is_array($array_part)){
+ $this->setIndexPath($array_part, $return_values, $key_path . $key . '.');
+ } else {
+ $return_values[] = array(
+ 'key_path' => $key_path . $key . '.',
+ 'value' => $array_part
+ );
+ }
+ }
+ }
+
+
+ public function changeOriginal($product)
+ {
+ $modifications = $this->settings['import_api_modification'];
+
+ foreach ($modifications as $field => $modification) {
+ //if (!$modification) {
+ if ($modification === '') {
+ continue;
+ }
+
+ if (in_array($field, ['price']) && !$product[$field]) {
+ continue;
+ }
+
+ if (!isset($product[$field])) {
+ $product[$field] = '';
+ }
+
+ if (in_array($field, ['image', 'images', 'cover']) && !$product[$field]) {
+ continue;
+ }
+
+ $modification = html_entity_decode($modification, ENT_QUOTES, 'UTF-8');
+
+ $modified = $this->check_string($modification, $product);
+ $product[$field] = $modified;
+
+ if (in_array($field, ['price', 'quantity', 'option_price','option_quantity', 'special', 'additional_shipping_cost', 'width', 'height', 'depth', 'weight', 'attribute_price', 'attribute_quantity', 'attribute_weight', 'minimal_quantity'])) {
+ $modified = $this->calculateMath($modified);
+ $product[$field] = $modified;
+ }
+ }
+
+ return $product;
+ }
+
+ public function checkCombinations($product)
+ {
+ $combinations = $this->settings['import_api_combination'];
+
+ foreach ($combinations as $field => $combination) {
+ if (!$combination || !isset($product[$field])) {
+ continue;
+ } else {
+ $position = 'all';
+ $parts = explode('##', $combination);
+ if(isset($parts[1])){
+ $position = $parts[1];
+ }
+ }
+
+ $exploded = $this->explodeCombinations($product[$field], $parts[0], $position);
+ $product[$field] = $exploded;
+
+ }
+ return $product;
+ }
+
+ public function explodeCombinations($value, $separator, $position = 'all')
+ {
+ if (!is_array($value)) {
+ $parts = explode($separator, $value);
+ if ($position == 'all') {
+ return $parts;
+ }
+
+ if ($position == 'last') {
+ return end($parts);
+ }
+
+ if (isset($parts[$position-1])) {
+ return $parts[$position-1];
+ }
+
+ return $value;
+ } else {
+ $ret = array();
+ foreach ($value as $key => $v) {
+ $ret[] = $this->explodeCombinations($v, $separator, $position);
+ }
+
+ return $ret;
+ }
+ }
+
+ public function getProductLinks()
+ {
+
+ return $this->product_links;
+ }
+
+ private function check_string($string, $prod)
+ {
+ if ($string) {
+ preg_match_all('/\[\[([a-z_]*\#?[^\]]*)\]\]/', $string , $match);
+ if ($match) {
+ $string = $this->replace_string($string, $match[1], $prod);
+ }
+ }
+ return $string;
+ }
+
+ private function replace_string($string, $keys, $d_array)
+ {
+ foreach ($keys as $key) {
+ $exploded_key = explode('#', $key);
+ $separator = false;
+
+ if (isset($exploded_key[1])) {
+ $string = str_replace($key, $exploded_key[0], $string);
+ $key = $exploded_key[0];
+ $separator = $exploded_key[1];
+ }
+
+ if (isset($d_array[$key])) {
+ if (is_array($d_array[$key])) {
+ $string = $this->replace_all_array_strings($d_array[$key], $key, $string, $separator);
+ } else {
+ $string = str_replace('[['. $key .']]', $d_array[$key], $string);
+ }
+ }
+ }
+ return $string;
+ }
+
+ private function replace_all_array_strings($array, $key, $string, $separator = false)
+ {
+ $s = array();
+ foreach ($array as $d_part) {
+ $s[] = str_replace('[['. $key .']]', $d_part, $string);
+ }
+
+ if ($separator !== false) {
+ $s = implode($separator, $s);
+ }
+
+ return $s;
+ }
+
+ private function calculateMath($expression)
+ {
+ $expression = str_replace(',', '.', $expression);
+ $expression = str_replace(' ', '', $expression);
+ $expression = preg_replace('/[^0-9\.\+\-\*\/\(\)]/', '', $expression);
+
+ $expression = $this->replaceMathExpession($expression, '*');
+ $expression = $this->replaceMathExpession($expression, '/');
+
+ $expression = $this->replaceMathExpession($expression, '+');
+ $expression = $this->replaceMathExpession($expression, '-');
+ return $expression;
+ }
+
+ private function replaceMathExpession($expression, $sign)
+ {
+ $patern = '\\' . $sign;
+ $expression = preg_replace_callback(
+ '/[0-9]*\.?[0-9]+' . $patern . '[0-9]*\.?[0-9]+(' . $patern . '[0-9]*\.?[0-9]+)*/',
+ function ($matches) use ($sign) {
+ if (!empty($matches[0])) {
+ return $this->calculate_string($matches[0], $sign);
+ }
+ }, $expression);
+ return $expression;
+ }
+
+ private function calculate_string($string, $sign)
+ {
+ $parts = explode($sign, $string);
+ $final = $parts[0];
+
+ for ($i = 1; $i < count($parts); $i++) {
+ if ($sign == '*') {
+ $final = $final * $parts[$i];
+ } elseif ($sign == '/') {
+ $final = $final / $parts[$i];
+ } elseif ($sign == '+') {
+ $final = $final + $parts[$i];
+ } elseif ($sign == '-') {
+ $final = $final - $parts[$i];
+ }
+ }
+ return $final;
+ }
+
+ public function jsonToArray($json_url)
+ {
+ $json_string = @file_get_contents($json_url);
+ $json_array = json_decode($json_string, true);
+ return $json_array;
+ }
+
+ public function setJsonArray($json_array)
+ {
+ $this->json_array = $json_array;
+ }
+
+ public function isSeq($array)
+ {
+ if (is_array($array) ) {
+ return is_int(array_key_first($array));
+ }
+ //return is_array($array) && isset($array[0]);
+ return false;
+ }
+}
diff --git a/modules/import_api/classes/index.php b/modules/import_api/classes/index.php
new file mode 100644
index 00000000..907720c1
--- /dev/null
+++ b/modules/import_api/classes/index.php
@@ -0,0 +1,35 @@
+
+* @copyright 2007-2018 PrestaShop SA
+* @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0)
+* International Registered Trademark & Property of PrestaShop SA
+*/
+
+header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
+header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT');
+
+header('Cache-Control: no-store, no-cache, must-revalidate');
+header('Cache-Control: post-check=0, pre-check=0', false);
+header('Pragma: no-cache');
+
+header('Location: ../');
+exit;
diff --git a/modules/import_api/classes/queue.php b/modules/import_api/classes/queue.php
new file mode 100644
index 00000000..f832baf8
--- /dev/null
+++ b/modules/import_api/classes/queue.php
@@ -0,0 +1,182 @@
+
+ * @copyright 2019 Dalibor Stojcevski
+ * @license Dalibor Stojcevski
+ */
+
+class Queue
+{
+ public $shop_name = 'default';
+
+ public function getQueued($file_id = 0, $limit = 0)
+ {
+
+ $sql = "SELECT * FROM " . _DB_PREFIX_ . "ia_temp t LEFT JOIN " . _DB_PREFIX_ . "ia_queues q ON (t.queue_id = q.queue_id) WHERE q.status < 3 AND t.shop = '" . pSQL($this->shop_name) . "'";
+
+ if ($file_id) {
+ $sql .= " AND t.file_id = " . (int)$file_id;
+ }
+
+ if ($limit) {
+ $sql .= " LIMIT " . (int)$limit;
+ }
+
+ $query = Db::getInstance()->executeS($sql);
+
+ return $query;
+ }
+
+ public function getLastQueue($file_id = 0, $status = 0)
+ {
+
+ $sql = "SELECT * FROM " . _DB_PREFIX_ . "ia_queues WHERE shop = '" . pSQL($this->shop_name) . "'";
+
+ if ($file_id) {
+ $sql .= " AND file_id = " . (int)$file_id;
+ }
+
+ if ($status) {
+ $sql .= " AND status = " . (int)$status;
+ }
+
+ $sql .= " ORDER BY date_added DESC";
+
+ $query = Db::getInstance()->executeS($sql);
+
+ if ($query) {
+ return $query[0];
+ } else {
+ return array();
+ }
+ }
+
+ public function getProducts($file_id = 0, $limit = 0)
+ {
+
+ $sql = "SELECT * FROM " . _DB_PREFIX_ . "ia_products p WHERE p.shop = '" . pSQL($this->shop_name) . "'";
+
+ if ($file_id) {
+ $sql .= " AND p.file_id = " . (int)$file_id;
+ }
+
+ if ($limit) {
+ $sql .= " LIMIT " . (int)$limit;
+ }
+
+ $query = Db::getInstance()->executeS($sql);
+
+ return $query;
+ }
+
+ public function deleteProduct($product_id = 0, $file_id = 0)
+ {
+
+ $sql = "DELETE FROM " . _DB_PREFIX_ . "ia_products WHERE product_id = '" . (int)$product_id . "'";
+
+ if ($file_id) {
+ $sql .= " AND file_id = " . (int)$file_id;
+ }
+ //print_r($sql); exit;
+
+ $query = Db::getInstance()->execute($sql);
+
+ return $query;
+ }
+
+ public function saveTempToDb($products, $file_id = 0) {
+
+ $this->deleteUnnecessary($file_id);
+
+ Db::getInstance()->execute("INSERT INTO " . _DB_PREFIX_ . "ia_queues SET shop = '" . pSQL($this->shop_name) . "', total = '" . count($products) . "', date_added = '" . time() . "', file_id = '" . (int)$file_id . "'");
+
+ $queue_id = Db::getInstance()->Insert_ID();
+
+ foreach($products as $indx => $product) {
+ Db::getInstance()->execute("INSERT INTO " . _DB_PREFIX_ . "ia_temp SET shop = '" . pSQL($this->shop_name) . "', date_added = '" . time() . "', indx = '" . pSQL($indx) . "', product = '" . pSQL(json_encode($product)) . "', queue_id = '" . (int)$queue_id . "', file_id = '" . (int)$file_id . "'");
+ }
+
+ return $queue_id;
+ }
+
+ public function deleteUnnecessary($file_id = 0, $shop = 'default') {
+ Db::getInstance()->execute("DELETE FROM " . _DB_PREFIX_ . "ia_queues WHERE shop = '" . pSQL($this->shop_name) . "' AND status < 3 AND file_id = " . (int)$file_id);
+ Db::getInstance()->execute("DELETE FROM " . _DB_PREFIX_ . "ia_temp WHERE shop = '" . pSQL($this->shop_name) . "' AND file_id = " . (int)$file_id);
+ }
+
+ public function getQueuedForTable($file_id = 0, $limit = 0) {
+
+ $products = array();
+
+ $queued = $this->getQueued($file_id, $limit);
+
+ foreach ($queued as $q) {
+ $p = json_decode($q['product'], true);
+ //var_dump($p); exit;
+ $products[] = array(
+ 'id' => $q['product_id'],
+ 'indx' => $q['indx'],
+ 'reference' => $p['reference'],
+ 'name' => $p['name'],
+ 'price' => $p['price'],
+ 'quantity' => $p['quantity'],
+ 'manufacturer' =>isset($p['brand']) ? $p['brand'] : '',
+ );
+ }
+
+ return $products;
+ }
+
+ public function getQueuedProduct($indx = 0, $file_id = 0) {
+
+ $sql = "SELECT * FROM " . _DB_PREFIX_ . "ia_temp WHERE shop = '" . pSQL($this->shop_name) . "'";
+
+ if ($indx) {
+ $sql .= " AND indx = '" . pSQL($indx) . "'";
+ }
+
+
+ if ($file_id) {
+ $sql .= " AND file_id = '" .(int)$file_id . "'";
+ }
+
+ $sql .= " LIMIT 1";
+
+ $query = Db::getInstance()->executeS($sql);
+
+ if ($query) {
+ return $query[0];
+ } else {
+ array();
+ }
+ }
+
+ public function deleteQueuedProduct($product_id = 0, $indx = 0, $file_id = 0) {
+
+ $sql = "DELETE FROM " . _DB_PREFIX_ . "ia_temp WHERE shop = '" . pSQL($this->shop_name) . "'";
+
+ if ($product_id) {
+ $sql .= " AND product_id = '" . (int)$product_id . "'";
+ }
+
+ if ($indx) {
+ $sql .= " AND indx = '" . pSQL($indx) . "'";
+ }
+
+ if ($file_id) {
+ $sql .= " AND file_id = '" .(int)$file_id . "'";
+ }
+
+ $sql .= " LIMIT 1";
+
+ $query = Db::getInstance()->execute($sql);
+
+ return $query;
+ }
+}
diff --git a/modules/import_api/config.xml b/modules/import_api/config.xml
new file mode 100644
index 00000000..e9369871
--- /dev/null
+++ b/modules/import_api/config.xml
@@ -0,0 +1,12 @@
+
+
+ import_api
+
+
+
+
+
+ 1
+ 1
+
+
\ No newline at end of file
diff --git a/modules/import_api/config_pl.xml b/modules/import_api/config_pl.xml
new file mode 100644
index 00000000..893d012d
--- /dev/null
+++ b/modules/import_api/config_pl.xml
@@ -0,0 +1,12 @@
+
+
+ import_api
+
+
+
+
+
+ 1
+ 1
+
+
\ No newline at end of file
diff --git a/modules/import_api/controllers/admin/AdminImport_ApiController.php b/modules/import_api/controllers/admin/AdminImport_ApiController.php
new file mode 100644
index 00000000..19e54827
--- /dev/null
+++ b/modules/import_api/controllers/admin/AdminImport_ApiController.php
@@ -0,0 +1,831 @@
+
+ * @copyright 2019 Dalibor Stojcevski
+ * @license Dalibor Stojcevski
+ */
+class AdminImport_ApiController extends ModuleAdminController
+{
+ public $bootstrap = true;
+
+ protected $_postErrors = array();
+
+ protected $fmr_fields;
+
+ public function __construct()
+ {
+ if ($this->context == null) {
+ $this->context = Context::getContext();
+ }
+ $this->toolbar_title = 'Sources';
+ $this->meta_title = 'Import api';
+ parent::__construct();
+ require_once(_PS_MODULE_DIR_ . 'import_api/classes/queue.php');
+ $this->queue = new Queue();
+ $this->frm_fields = ['unique', 'reference', 'name', 'description', 'short_description', 'price', 'wholesale_price', 'quantity', 'images', 'cover', 'brand', 'category', 'category_parent', 'ean13', 'upc', 'condition', 'location', 'additional_shipping_cost', 'width', 'height', 'depth', 'weight', 'active', 'feature', 'feature_value', 'attribute1', 'attribute_value1', 'attribute2', 'attribute_value2', 'attribute3', 'attribute_value3', 'attribute_price', 'attribute_quantity', 'attribute_ean', 'attribute_weight', 'minimal_quantity'];
+ $this->replace_fields = ['reference', 'name', 'description', 'short_description', 'price', 'wholesale_price', 'quantity', 'images', 'cover', 'brand', 'category', 'ean13', 'upc', 'condition', 'location', 'width', 'height', 'depth', 'weight', 'feature', 'feature_value', 'attribute1', 'attribute_value1', 'attribute2', 'attribute_value2', 'attribute3', 'attribute_value3', 'minimal_quantity'];
+ $this->frm_settings = array('top_category' => '', 'default_category' => '', 'default_brand' => '', 'price_multiplier' => '', 'combination_price_multiplier' => '', 'add_combination_price' => 0, 'category_path' => 0, 'id_tax_rules_group' => -1, 'same_update' => 0, 'not_existing' => 0, 'only_update' => 0, 'synchronize_field' => 'automatic');
+ $this->frm_update_settings = array('quantity' => 1, 'price' => 0, 'wholesale_price' => 0, 'manufacturer' => 0, 'category' => 0, 'cover' => 0, 'images' => 0, 'name' => 0, 'description' => 0, 'short_description' => 0,'reference' => 0, 'ean13' => 0, 'ups' => 0, 'condition' => 0, 'location'=> 0, 'additional_shipping_cost'=> 0, 'width' => 0 , 'height' => 0 , 'depth' => 0 , 'weight' => 0, 'features' => 0, 'attributes' => 0, 'active' => 0, 'id_tax_rules_group' => 0, 'minimal_quantity' => 0);
+ $this->filter_fields = ['reference', 'name', 'price', 'wholesale_price', 'quantity', 'images', 'cover', 'brand', 'category', 'ean13', 'upc', 'condition', 'location', 'width', 'height', 'depth', 'weight', 'feature', 'feature_value', 'attribute1', 'attribute_value1', 'attribute2', 'attribute_value2', 'attribute3', 'attribute_value3', 'minimal_quantity'];
+
+
+
+ if (Tools::getValue('add_source')) {
+ $this->addSource();
+ } elseif (Tools::getValue('delete') && Tools::getValue('file_id')) {
+ $this->deleteProducts(Tools::getValue('file_id'));
+ } elseif (Tools::getValue('file_id')) {
+ if (((bool)Tools::isSubmit('submitImportSettings')) == true) {
+ $this->_postSettingsValidation();
+ $message = '';
+ if (!count($this->_postErrors)) {
+ $this->_saveSettings(Tools::getValue('file_id'));
+ } else {
+ foreach ($this->_postErrors as $err) {
+ $this->errors[] = $err;
+ }
+ }
+
+ }
+ $this->processFile(Tools::getValue('file_id'), Tools::getValue('type'));
+ } else {
+ $this->getSourceList();
+ }
+ //return;
+ }
+
+
+ public function addSource()
+ {
+ if (Tools::isSubmit('submitAddSource')) {
+ $this->postFileProcess();
+ }
+ $this->context->smarty->assign('form_link', $this->context->link->getAdminLink('AdminImport_Api&add_source=1&token=' . Tools::getAdminTokenLite('AdminImport_Api'), false));
+ $file_tpl = $this->context->smarty->fetch(_PS_MODULE_DIR_ . 'import_api/views/templates/admin/file_upload.tpl');
+ $this->content .= $file_tpl;
+ }
+
+ public function deleteProducts($file_id)
+ {
+ $query_file = Db::getInstance()->executeS("SELECT * FROM ". _DB_PREFIX_ . "ia_files WHERE file_id = '" . (int)$file_id . "' LIMIT 1");
+ if (empty($query_file)) {
+ $this->errors[] = $this->my_translation('File not found');
+ return;
+ } else {
+ $file = $query_file[0];
+ }
+
+ $this->toolbar_title = 'Delete';
+
+ $query_total = Db::getInstance()->executeS("SELECT COUNT(*) as total FROM ". _DB_PREFIX_ . "ia_products WHERE file_id = '" . (int)$file_id . "'");
+
+ $total = $query_total ? $query_total[0]['total'] : 0;
+
+ $file_name = $file['name'] ? $file['name'] : $file['link'];
+ $this->context->smarty->assign('token', $file['mask'] . $file['date_added']);
+ $this->context->smarty->assign('get_import_link', $this->context->link->getModuleLink('import_api', 'import', array('file_id' => $file_id)));
+
+ $this->context->smarty->assign('file_name', $file_name);
+ $this->context->smarty->assign('total', $total);
+ $delete_tpl = $this->context->smarty->fetch(_PS_MODULE_DIR_ . 'import_api/views/templates/admin/delete.tpl');
+ $this->content .= $delete_tpl;
+ }
+
+ public function getSourceList()
+ {
+ if (Tools::getValue('status_file_id') && Tools::getValue('change_status') !== false) {
+ Db::getInstance()->execute("UPDATE ". _DB_PREFIX_ . "ia_files SET status = '" . (int)Tools::getValue('change_status') ."' WHERE file_id = " . (int)Tools::getValue('status_file_id'));
+ }
+
+ $query_files = Db::getInstance()->executeS("SELECT f.* FROM ". _DB_PREFIX_ . "ia_files f ORDER BY date_edited DESC, file_id DESC");
+
+ $files = array();
+ if ($query_files) {
+ foreach ($query_files as $file) {
+ $other_status = $file['status'] ? 0 : 1;
+ $other_status_text = $file['status'] ? 'Disable' : 'Enable';
+ if (!Tools::getValue('show_disabled') && !$file['status']) {
+ continue;
+ }
+ $files[] = array(
+ 'file_id' => $file['file_id'],
+ 'name' => $file['name'] ? $file['name'] : $file['link'],
+ 'date_added' => date('F j, Y', $file['date_added']),
+ 'process_link' => $this->context->link->getAdminLink('AdminImport_Api&file_id=' . $file['file_id'] . '&token=' . Tools::getAdminTokenLite('AdminImport_Api'), false),
+ 'change_status_link' => $this->context->link->getAdminLink('AdminImport_Api&change_status=' . $other_status . '&status_file_id=' . $file['file_id'] . '&token=' . Tools::getAdminTokenLite('AdminImport_Api'), false),
+ 'other_status_text' => $other_status_text
+ );
+ }
+
+ $this->context->smarty->assign('files', $files);
+ $this->context->smarty->assign('show_disabled_link', $this->context->link->getAdminLink('AdminImport_Api&show_disabled=1&token=' . Tools::getAdminTokenLite('AdminImport_Api'), false));
+
+ $list_tpl = $this->context->smarty->fetch(_PS_MODULE_DIR_ . 'import_api/views/templates/admin/list.tpl');
+ $this->content .= $list_tpl;
+ } else {
+ $this->addSource();
+ }
+ }
+
+ public function processFile($file_id, $type = 'import')
+ {
+ $this->addJquery();
+ $this->addJS(_MODULE_DIR_. 'import_api/views/js/dataTables.js');
+ $this->addJS(_MODULE_DIR_. 'import_api/views/js/dataTables.bootstrap4.js');
+ $this->addCSS(_MODULE_DIR_. 'import_api/views/css/dataTables.bootstrap4.css');
+ $query_file_settings = Db::getInstance()->executeS("SELECT f.*, fs.mapping FROM ". _DB_PREFIX_ . "ia_files f LEFT JOIN ". _DB_PREFIX_ . "ia_file_settings fs ON(fs.file_id = f.file_id) WHERE f.file_id = '" . (int)$file_id . "' LIMIT 1");
+ if (empty($query_file_settings)) {
+ $this->errors[] = $this->my_translation('File not found');
+ } else {
+ $file_settings = $query_file_settings[0];
+ $file_name = $file_settings['name'] ? $file_settings['name'] : $file_settings['link'];
+ if ($file_settings['mapping'] && $type == 'import') {
+
+ $this->toolbar_title = 'Import';
+ $this->context->smarty->assign('get_import_link', $this->context->link->getModuleLink('import_api', 'import', array('file_id'=> $file_id)));
+ $this->context->smarty->assign('show_all_link', $this->context->link->getAdminLink('AdminImport_Api&type=import&showall=1&file_id=' . $file_id . '&token=' . Tools::getAdminTokenLite('AdminImport_Api'), false));
+ $this->context->smarty->assign('delete_link', $this->context->link->getAdminLink('AdminImport_Api&delete=1&file_id=' . $file_id . '&token=' . Tools::getAdminTokenLite('AdminImport_Api'), false));
+
+ if (Tools::getValue('showall')) {
+ $limit = 0;
+ } else {
+ $limit = 600;
+ }
+
+ $products = $this->queue->getQueuedForTable($file_id, $limit);
+ $this->context->smarty->assign('settings_link', $this->context->link->getAdminLink('AdminImport_Api&type=settings&file_id=' . $file_id . '&token=' . Tools::getAdminTokenLite('AdminImport_Api'), false));
+
+ $this->context->smarty->assign('products', $products);
+ $this->context->smarty->assign('limit', $limit);
+ $this->context->smarty->assign('auto_queue', 0);
+ $this->context->smarty->assign('file_name', $file_name);
+ $output = $this->context->smarty->fetch(_PS_MODULE_DIR_ . 'import_api/views/templates/admin/import.tpl');
+ $queue_tpl = $this->context->smarty->fetch(_PS_MODULE_DIR_ . 'import_api/views/templates/admin/temp.tpl');
+ $this->content .= $queue_tpl;
+ $this->content .= $output;
+ } else {
+ $this->toolbar_title = 'Settings';
+ $this->context->smarty->assign('file_settings', $file_settings);
+ $this->context->smarty->assign('file_id', $file_id);
+ $this->context->smarty->assign('file_name', $file_name);
+ $this->context->smarty->assign('import_link', $this->context->link->getAdminLink('AdminImport_Api&type=import&file_id=' . $file_id . '&token=' . Tools::getAdminTokenLite('AdminImport_Api'), false));
+ $this->context->smarty->assign('get_fields_link', $this->context->link->getModuleLink('import_api', 'ajax', array('file_id'=> $file_id)));
+ $output = $this->context->smarty->fetch(_PS_MODULE_DIR_ . 'import_api/views/templates/admin/configure.tpl');
+ //$import_html = $this->context->smarty->fetch(_PS_MODULE_DIR_ . 'import_api/views/templates/admin/import.tpl');
+
+ $this->content .= $output. $this->renderSettingsForm($file_settings);
+ //$this->content .= $output;
+ }
+ }
+ }
+ public static function displayHumanReadableSize($size)
+ {
+ return Tools::formatBytes($size);
+ }
+
+ public function initPageHeaderToolbar()
+ {
+ $this->page_header_toolbar_btn['new_attachment'] = array(
+ 'href' => self::$currentIndex . '&add_source=1&token=' . $this->token,
+ 'desc' => $this->my_translation('Add new source'),
+ 'icon' => 'process-icon-new',
+ );
+ $this->page_header_toolbar_btn['view_list'] = array(
+ 'href' => self::$currentIndex . '&token=' . $this->token,
+ 'desc' => $this->my_translation('View list'),
+ 'icon' => 'process-icon-list',
+ );
+ parent::initPageHeaderToolbar();
+ }
+
+ public function postFileProcess()
+ {
+ $shop = 'default';
+ $source = !empty($_POST['import_api_type']) ? $_POST['import_api_type'] : '';
+ $headers = !empty($_POST['import_api_headers']) ? $_POST['import_api_headers'] : '';
+ $delimiter = !empty($_POST['import_api_delimiter']) ? $_POST['import_api_delimiter'] : '';
+ $mask = uniqid();
+ if (!empty($_POST['import_api_link'])) {
+
+ $query_file = Db::getInstance()->executeS("SELECT * FROM ". _DB_PREFIX_ . "ia_files WHERE link = '" . pSQL($_POST['import_api_link']) . "' AND shop = '" . pSQL($shop) . "' AND status = 1 LIMIT 1");
+
+ if ($query_file) {
+ $file_id = $query_file[0]['file_id'];
+ Db::getInstance()->execute("UPDATE ". _DB_PREFIX_ . "ia_files SET link = '" . pSQL($_POST['import_api_link']) . "', delimiter = '" . pSQL($delimiter) . "', headers = '" . pSQL($headers) . "', source = '" . pSQL($source) . "', date_edited = '" . time() . "' WHERE file_id = " . (int)$file_id ." AND shop = '" . pSQL($shop) . "'");
+
+ } else {
+ Db::getInstance()->execute("INSERT INTO ". _DB_PREFIX_ . "ia_files SET link = '" . pSQL($_POST['import_api_link']) . "', shop = '" . pSQL($shop) . "', delimiter = '" . pSQL($delimiter) . "', headers = '" . pSQL($headers) . "', source = '" . pSQL($source) . "', mask = '" . pSQL($mask) ."', date_added = '" . time() . "'");
+ }
+ } elseif (!empty($_FILES['doc']['tmp_name'])) {
+
+ $name = $_FILES['doc']['name'];
+ $tmp_name = $_FILES['doc']['tmp_name'];
+ $type = $_FILES['doc']['type'];
+
+ $file_ext = pathinfo($name, PATHINFO_EXTENSION);
+
+ if (in_array($file_ext, ['zip'])) {
+ $this->errors[] = $this->my_translation("Sorry, app can't read zip files. Extract zip and upload content from inside");
+ } else {
+ $target_file = _PS_MODULE_DIR_ . 'import_api/views/img/' . $mask . '_' . $name;
+ $base_link = Tools::getHttpHost(true).__PS_BASE_URI__;
+ $target_url = $base_link. 'modules/import_api/views/img/' . $mask . '_' . $name;;
+ if (move_uploaded_file($tmp_name, $target_file)) {
+ Db::getInstance()->execute("INSERT INTO ". _DB_PREFIX_ . "ia_files SET link = '" . pSQL($target_url) ."', mime_type = '" . pSQL($type) ."', source = '" . pSQL($source) ."', delimiter = '" . pSQL($delimiter) . "', headers = '" . pSQL($headers) . "', name = '" . pSQL($name) ."', mask = '" . pSQL($mask) ."', shop = '" . pSQL($shop) . "', date_added = '" . time() . "'");
+ } else {
+ $this->errors[] = $this->my_translation("Sorry, there was an error uploading your file.");
+ }
+ }
+ } else {
+ $this->errors[] = $this->my_translation("Enter link or upload file.");
+ }
+ if (!$this->errors) {
+ if (!isset($file_id)) {
+ $file_id = Db::getInstance()->Insert_ID();
+ }
+ if (!empty($_POST['import_api_link'])) {
+ if( strpos($_POST['import_api_link'], 'convert') !== false) {
+ $fields_file_path = _PS_MODULE_DIR_ . 'import_api/fields.txt';
+ $file_settings_file_path = _PS_MODULE_DIR_ . 'import_api/file_settings.txt';
+ if (file_exists($fields_file_path)) {
+ $f = file_get_contents($fields_file_path);
+ Db::getInstance()->execute("UPDATE ". _DB_PREFIX_ . "ia_files SET fields = '" . pSQL($f) . "', date_edited = '" . time() . "' WHERE file_id = " . (int)$file_id);
+ }
+ if (file_exists($file_settings_file_path)) {
+ $sql_raw = file_get_contents($file_settings_file_path);
+ $sql = sprintf($sql_raw, _DB_PREFIX_, $file_id);
+
+ Db::getInstance()->execute($sql);
+
+ }
+ }
+ }
+ Tools::redirectAdmin($this->context->link->getAdminLink('AdminImport_Api&type=settings&file_id=' . $file_id . '&token=' . Tools::getAdminTokenLite('AdminImport_Api'), false));
+ }
+ }
+
+ protected function renderSettingsForm($settings, $tab = 'general')
+ {
+ $helper = new HelperForm();
+ $this->tab = $tab;
+ $helper->show_toolbar = false;
+ $helper->table = $this->table;
+ //$helper->module = $this;
+ $helper->default_form_language = $this->context->language->id;
+ $helper->allow_employee_form_lang = Configuration::get('PS_BO_ALLOW_EMPLOYEE_FORM_LANG', 0);
+
+ $helper->identifier = $this->identifier;
+ $helper->submit_action = 'submitImportSettings';
+ $helper->currentIndex = $this->context->link->getAdminLink('AdminImport_Api', false)
+ .'&type=settings&file_id='. $settings['file_id'];
+ $helper->token = Tools::getAdminTokenLite('AdminImport_Api');
+
+ $helper->tpl_vars = array(
+ 'fields_value' => $this->getConfigFormValues(),
+ 'languages' => $this->context->controller->getLanguages(),
+ 'id_language' => $this->context->language->id,
+ );
+
+ return $helper->generateForm(array($this->getConfigForm($settings)));
+ }
+
+ protected function getConfigForm($settings)
+ {
+
+ $import_api_fields = $settings['fields'] ? html_entity_decode($settings['fields'], ENT_QUOTES, 'UTF-8') : '';
+
+ $source_fields = explode('##', $import_api_fields);
+
+
+
+ $options_not_existing[] = array(
+ 'id_option' => 0,
+ 'name' => $this->my_translation('Keep it'),
+ );
+
+ $options_not_existing[] = array(
+ 'id_option' => 1,
+ 'name' => $this->my_translation('Set quantity 0'),
+ );
+
+ $options_not_existing[] = array(
+ 'id_option' => 2,
+ 'name' => $this->my_translation('Set status disabled'),
+ );
+
+ $options_not_existing[] = array(
+ 'id_option' => 3,
+ 'name' => $this->my_translation('Delete'),
+ );
+
+ $options_synchronize_field[] = array(
+ 'id_option' => 'automatic',
+ 'name' => $this->my_translation('From unique field')
+ );
+
+ $options_synchronize_field[] = array(
+ 'id_option' => 'reference',
+ 'name' => $this->my_translation('Reference')
+ );
+
+ $options_synchronize_field[] = array(
+ 'id_option' => 'ean13',
+ 'name' => $this->my_translation('Ean13')
+ );
+
+ $options_synchronize_field[] = array(
+ 'id_option' => 'upc',
+ 'name' => $this->my_translation('Upc')
+ );
+
+ $options_synchronize_field[] = array(
+ 'id_option' => 'name',
+ 'name' => $this->my_translation('Product name')
+ );
+
+ $options_synchronize_field[] = array(
+ 'id_option' => 'id_product',
+ 'name' => $this->my_translation('Prestashop product id')
+ );
+
+
+ $options_yes_no[] = array(
+ 'id_option' => 0,
+ 'name' => $this->my_translation('No'),
+ );
+ $options_yes_no[] = array(
+ 'id_option' => 1,
+ 'name' => $this->my_translation('Yes'),
+ );
+
+ $options[] = array(
+ 'id_option' => 0,
+ 'name' => $this->my_translation('--Please select--'),
+ );
+
+ $filter_options[] = array(
+ 'id_option' => 'equal',
+ 'name' => $this->my_translation('Equal'),
+ );
+
+ $filter_options[] = array(
+ 'id_option' => 'not_equal',
+ 'name' => $this->my_translation('Not equal'),
+ );
+
+ $filter_options[] = array(
+ 'id_option' => 'greater',
+ 'name' => $this->my_translation('Greater'),
+ );
+
+ $filter_options[] = array(
+ 'id_option' => 'less',
+ 'name' => $this->my_translation('Less'),
+ );
+
+ $filter_options[] = array(
+ 'id_option' => 'not_empty',
+ 'name' => $this->my_translation('Not empty'),
+ );
+
+ $filter_options[] = array(
+ 'id_option' => 'regexp',
+ 'name' => $this->my_translation('REGEXP'),
+ );
+
+ if($import_api_fields){
+ foreach($source_fields as $field){
+ $options[] = array(
+ 'id_option' => $field,
+ 'name' => $field,
+ );
+ }
+ }
+
+ $def_tax[] = array('id_tax_rules_group' => -1, 'name' => 'Default');
+ $def_tax[] = array('id_tax_rules_group' => 0, 'name' => 'No tax');
+
+ $taxes = array_merge($def_tax, TaxRulesGroup::getTaxRulesGroups());
+
+ $fields[] = array(
+ 'type' => 'hidden',
+ 'id' => 'fields',
+ 'name' => 'import_api_fields',
+ );
+
+ $fields[] = array(
+ 'type' => 'hidden',
+ 'id' => 'import_api_link',
+ 'name' => 'import_api_link',
+ );
+
+ $fields[] = array(
+ 'type' => 'hidden',
+ 'id' => 'import_api_type',
+ 'name' => 'import_api_type',
+ );
+
+ foreach($this->frm_fields as $fmr_field){
+ $fields[] = array(
+ 'type' => 'select',
+ 'class' => 'api_field',
+ 'tab' => 'general',
+ 'required' => in_array($fmr_field, ['unique', 'name']) ? true : false,
+ 'label' => $this->my_translation($fmr_field),
+ 'name' => 'import_api_field['. $fmr_field .']',
+ 'options' => array(
+ 'query' => $options,
+ 'id' => 'id_option',
+ 'name' => 'name'
+ )
+ );
+ }
+
+ foreach($this->frm_fields as $fmr_field){
+ $fields[] = array(
+ 'type' => 'text',
+ 'tab' => 'modifications',
+ 'label' => $this->my_translation($fmr_field),
+ 'name' => 'import_api_modification['. $fmr_field .']',
+ );
+
+ $fields[] = array(
+ 'type' => 'text',
+ 'tab' => 'combinations',
+ 'label' => $this->my_translation($fmr_field),
+ 'name' => 'import_api_split['. $fmr_field .']',
+ );
+ }
+
+ foreach($this->filter_fields as $f_field) {
+ $fields[] = array(
+ 'type' => 'textarea',
+ 'tab' => 'filter',
+ 'rows' => 10,
+ 'hint' => 'Enter each value in new line',
+ 'label' => $this->my_translation($f_field),
+ 'name' => 'import_api_filter['. $f_field .']',
+ 'description' => 'Enter values, each in new line',
+ );
+
+ $fields[] = array(
+ 'type' => 'select',
+ 'tab' => 'filter',
+ 'label' => $this->my_translation('Filter options for ' . $f_field),
+ 'name' => 'import_api_filter_options['. $f_field .']',
+ 'options' => array(
+ 'query' => $filter_options,
+ 'id' => 'id_option',
+ 'name' => 'name'
+ )
+ );
+ }
+
+ foreach($this->replace_fields as $r_field) {
+ $fields[] = array(
+ 'type' => 'textarea',
+ 'tab' => 'replace',
+ 'rows' => 10,
+ 'hint' => 'Enter each value in new line in format OLD_TEXT##NEW_TEXT',
+ 'label' => $this->my_translation($r_field),
+ 'name' => 'import_api_replace['. $r_field .']',
+ 'description' => 'Enter values in format old_word##new_word',
+ );
+ }
+
+ // Settings fields
+
+ $fields[] = array(
+ 'type' => 'text',
+ 'tab' => 'settings',
+ 'label' => $this->my_translation('Top category'),
+ 'name' => 'import_api_settings[top_category]',
+ );
+
+ $fields[] = array(
+ 'type' => 'text',
+ 'tab' => 'settings',
+ 'label' => $this->my_translation('Default category'),
+ 'name' => 'import_api_settings[default_category]',
+ );
+
+ $fields[] = array(
+ 'type' => 'text',
+ 'tab' => 'settings',
+ 'label' => $this->my_translation('Default brand'),
+ 'name' => 'import_api_settings[default_brand]',
+ );
+
+ $fields[] = array(
+ 'type' => 'text',
+ 'tab' => 'settings',
+ 'label' => $this->my_translation('Price multiplier'),
+ 'name' => 'import_api_settings[price_multiplier]',
+ );
+
+ $fields[] = array(
+ 'type' => 'text',
+ 'tab' => 'settings',
+ 'label' => $this->my_translation('Combination price multiplier'),
+ 'name' => 'import_api_settings[combination_price_multiplier]',
+ );
+
+ $fields[] = array(
+ 'type' => 'select',
+ 'tab' => 'settings',
+ 'label' => $this->my_translation('Add combination price to product price'),
+ 'name' => 'import_api_settings[add_combination_price]',
+ 'options' => array(
+ 'query' => $options_yes_no,
+ 'id' => 'id_option',
+ 'name' => 'name'
+ )
+ );
+
+ $fields[] = array(
+ 'type' => 'select',
+ 'tab' => 'settings',
+ 'label' => $this->my_translation('Create category path from array'),
+ 'name' => 'import_api_settings[category_path]',
+ 'options' => array(
+ 'query' => $options_yes_no,
+ 'id' => 'id_option',
+ 'name' => 'name'
+ )
+ );
+
+ $fields[] = array(
+ 'type' => 'select',
+ 'tab' => 'settings',
+ 'label' => $this->my_translation('Tax rules group'),
+ 'name' => 'import_api_settings[id_tax_rules_group]',
+ 'options' => array(
+ 'query' => $taxes,
+ 'id' => 'id_tax_rules_group',
+ 'name' => 'name'
+ )
+ );
+
+
+ $fields[] = array(
+ 'type' => 'select',
+ 'tab' => 'settings',
+ 'label' => $this->my_translation('Update even if file is not changed'),
+ 'name' => 'import_api_settings[same_update]',
+ 'options' => array(
+ 'query' => $options_yes_no,
+ 'id' => 'id_option',
+ 'name' => 'name'
+ )
+ );
+
+ $fields[] = array(
+ 'type' => 'select',
+ 'tab' => 'settings',
+ 'label' => $this->my_translation('If product not exist in file anymore'),
+ 'name' => 'import_api_settings[not_existing]',
+ 'options' => array(
+ 'query' => $options_not_existing,
+ 'id' => 'id_option',
+ 'name' => 'name'
+ )
+ );
+
+ $fields[] = array(
+ 'type' => 'select',
+ 'tab' => 'settings',
+ 'label' => $this->my_translation('Update only'),
+ 'name' => 'import_api_settings[only_update]',
+ 'options' => array(
+ 'query' => $options_yes_no,
+ 'id' => 'id_option',
+ 'name' => 'name'
+ )
+ );
+
+ $fields[] = array(
+ 'type' => 'select',
+ 'tab' => 'settings',
+ 'label' => $this->my_translation('Update field'),
+ 'name' => 'import_api_settings[synchronize_field]',
+ 'options' => array(
+ 'query' => $options_synchronize_field,
+ 'id' => 'id_option',
+ 'name' => 'name'
+ )
+ );
+
+ foreach($this->frm_update_settings as $key => $setting) {
+ $fields[] = array(
+ 'type' => 'select',
+ 'tab' => 'update',
+ 'label' => $this->my_translation('Update '. $key),
+ 'name' => 'import_api_settings[update_' . $key .']',
+ 'options' => array(
+ 'query' => $options_yes_no,
+ 'id' => 'id_option',
+ 'name' => 'name'
+ )
+ );
+ }
+
+ return array(
+ 'form' => array(
+ 'legend' => array(
+ 'title' => $this->my_translation('Configuration'),
+ 'icon' => 'icon-cogs',
+ ),
+ 'input' => $fields,
+ 'submit' => array(
+ 'title' => $this->my_translation('Save'),
+ ),
+ 'tabs' => array(
+ 'general' => $this->my_translation('Assign'),
+ 'combinations' => $this->my_translation('Split'),
+ 'modifications' => $this->my_translation('Modifications'),
+ 'filter' => $this->my_translation('Filter'),
+ 'replace' => $this->my_translation('Replace'),
+ 'settings' => $this->my_translation('Settings'),
+ 'update' => $this->my_translation('Update fields'),
+ 'test' => $this->my_translation('Test'),
+ ),
+ ),
+ );
+ }
+
+ protected function getConfigFormValues()
+ {
+ $file_id = Tools::getValue('file_id');
+ $return_settings = array();
+ if (((bool)Tools::isSubmit('submitImportSettings')) == true) {
+ $config_fields = Tools::getValue('import_api_field');
+ $config_modification = Tools::getValue('import_api_modification');
+ $config_split = Tools::getValue('import_api_split');
+ $config_replace = Tools::getValue('import_api_replace');
+ $config_filter = Tools::getValue('import_api_filter');
+ $config_filter_options = Tools::getValue('import_api_filter_options');
+
+ $config_settings = Tools::getValue('import_api_settings');
+
+ $return_settings['import_api_fields'] = Tools::getValue('import_api_fields');
+ $return_settings['import_api_link'] = Tools::getValue('import_api_link');
+ $return_settings['import_api_type'] = Tools::getValue('import_api_type');
+ } else {
+ $query_file_settings = Db::getInstance()->executeS("SELECT fs.* FROM ". _DB_PREFIX_ . "ia_file_settings fs WHERE fs.file_id = '" . (int)$file_id . "' LIMIT 1");
+ if ($query_file_settings) {
+ $file_settings =$query_file_settings[0];
+ $config_fields = json_decode($file_settings['mapping'], true);
+ $config_modification = json_decode($file_settings['modification'], true);
+ $config_split = json_decode($file_settings['split'], true);
+ $config_replace = json_decode($file_settings['replace'], true);
+ $config_filter = json_decode($file_settings['filter'], true);
+ $config_filter_options = json_decode($file_settings['filter_options'], true);
+ $config_settings = json_decode($file_settings['settings'], true);
+ }
+
+ $return_settings['import_api_fields'] = '';
+ $return_settings['import_api_link'] = '';
+ $return_settings['import_api_type'] = '';
+ }
+
+ foreach($this->frm_fields as $fmr_field){
+ $return_settings['import_api_field[' . $fmr_field . ']'] = !empty($config_fields[$fmr_field]) ? html_entity_decode($config_fields[$fmr_field], ENT_QUOTES, 'UTF-8') : '';
+ $return_settings['import_api_modification[' . $fmr_field . ']'] = !empty($config_modification[$fmr_field]) ? html_entity_decode($config_modification[$fmr_field], ENT_QUOTES, 'UTF-8') : '';
+ $return_settings['import_api_split[' . $fmr_field . ']'] = !empty($config_split[$fmr_field]) ? html_entity_decode($config_split[$fmr_field], ENT_QUOTES, 'UTF-8') : '';
+ }
+
+ foreach($this->replace_fields as $r_field){
+ $return_settings['import_api_replace[' . $r_field . ']'] = !empty($config_replace[$r_field]) ? html_entity_decode($config_replace[$r_field], ENT_QUOTES, 'UTF-8') : '';
+ }
+
+ foreach($this->filter_fields as $f_field){
+ $return_settings['import_api_filter[' . $f_field . ']'] = !empty($config_filter[$f_field]) ? html_entity_decode($config_filter[$f_field], ENT_QUOTES, 'UTF-8') : '';
+ $return_settings['import_api_filter_options[' . $f_field . ']'] = !empty($config_filter_options[$f_field]) ? html_entity_decode($config_filter_options[$f_field], ENT_QUOTES, 'UTF-8') : '';
+ }
+
+ for($i = 1; $i <= 3; $i++){
+ $return_settings['import_api_field[field' . $i. ']'] = !empty($config_fields['field' . $i]) ? html_entity_decode($config_fields['field' . $i], ENT_QUOTES, 'UTF-8') : '';
+ }
+
+ for($i = 1; $i <= 3; $i++){
+ $return_settings['import_api_modification[modification' . $i . ']'] = !empty($config_modification['modification' . $i]) ? html_entity_decode($config_modification['modification' . $i], ENT_QUOTES, 'UTF-8') : '';
+ }
+
+ foreach($this->frm_settings as $setting => $value){
+ $return_settings['import_api_settings[' . $setting . ']'] = isset($config_settings[$setting]) ? $config_settings[$setting] : $value;
+ }
+
+ foreach($this->frm_update_settings as $setting => $value){
+ $return_settings['import_api_settings[update_' . $setting . ']'] = isset($config_settings['update_' . $setting]) ? $config_settings['update_' . $setting] : $value;
+ }
+
+
+ return $return_settings;
+ }
+
+ protected function _postSettingsValidation()
+ {
+
+ if (!Tools::getValue('import_api_field')) {
+ $this->_postErrors[] = $this->my_translation('You must assign unique field and name field.');
+ } else {
+ $fields = Tools::getValue('import_api_field');
+ if(empty($fields['unique'])){
+ $this->_postErrors[] = $this->my_translation('You must assign unique field');
+ }
+ if(empty($fields['name'])){
+ //$this->_postErrors[] = $this->my_translation('You must assign name field');
+ }
+ }
+ }
+ protected function _saveSettings($file_id, $shop = 'default')
+ {
+
+ $mapping = '';
+ $modification = '';
+ $split = '';
+ $replace = '';
+ $filter = '';
+ $filter_options = '';
+ $settings = '';
+ $update = false;
+
+ $query_file_settings = Db::getInstance()->executeS("SELECT fs.* FROM ". _DB_PREFIX_ . "ia_file_settings fs WHERE fs.file_id = '" . (int)$file_id . "' LIMIT 1");
+ if ($query_file_settings) {
+ $file_settings = $query_file_settings[0];
+ $mapping = $file_settings['mapping'];
+ $modification = $file_settings['modification'];
+ $split = $file_settings['split'];
+ $replace = $file_settings['replace'];
+ $filter = $file_settings['filter'];
+ $filter_options = $file_settings['filter_options'];
+ $settings = $file_settings['settings'];
+ $update = true;
+ }
+
+ $implode = array();
+
+ $new_mapping = json_encode(Tools::getValue('import_api_field'));
+ $new_modification = json_encode(Tools::getValue('import_api_modification'));
+ $new_split = json_encode(Tools::getValue('import_api_split'));
+ $new_replace = json_encode(Tools::getValue('import_api_replace'));
+ $new_filter = json_encode(Tools::getValue('import_api_filter'));
+ $new_filter_options = json_encode(Tools::getValue('import_api_filter_options'));
+ $new_settings = json_encode(Tools::getValue('import_api_settings'));
+
+ if (Tools::getValue('import_api_field') && $new_mapping != $mapping) {
+ $implode[] = "mapping='" . pSQL($new_mapping). "'";
+ }
+
+ if (Tools::getValue('import_api_modification') && $new_modification != $modification) {
+ $implode[] = "modification='" . pSQL($new_modification). "'";
+ }
+
+ if (Tools::getValue('import_api_split') && $new_split != $split) {
+ $implode[] = "split='" . pSQL($new_split). "'";
+ }
+
+ if (Tools::getValue('import_api_replace') && $new_replace != $replace) {
+ $implode[] = "`replace`='" . pSQL($new_replace). "'";
+ }
+
+ if (Tools::getValue('import_api_filter') && $new_filter != $filter) {
+ $implode[] = "`filter`='" . pSQL($new_filter). "'";
+ }
+
+ if (Tools::getValue('import_api_filter_options') && $new_filter_options != $filter_options) {
+ $implode[] = "filter_options='" . pSQL($new_filter_options). "'";
+ }
+
+ if (Tools::getValue('import_api_settings') && $new_settings != $settings) {
+ $implode[] = "settings='" . pSQL($new_settings). "'";
+ }
+
+ if ($implode) {
+ $implode[] = "date_updated = '" . time() ."'";
+ $implode_string = implode(',', $implode);
+
+ if ($update) {
+ Db::getInstance()->execute("UPDATE ". _DB_PREFIX_ . "ia_file_settings SET " .$implode_string. " WHERE file_id = '" .(int)$file_id . "' AND shop = '" . pSQL($shop) . "'");
+ } else {
+ Db::getInstance()->execute("INSERT INTO ". _DB_PREFIX_ . "ia_file_settings SET " .$implode_string. ", file_id = '" .(int)$file_id . "', shop = '" . pSQL($shop) . "'");
+ }
+ //print_r("UPDATE ". _DB_PREFIX_ . "ia_file_settings SET " .$implode_string. " WHERE file_id = '" .(int)$file_id . "' AND shop = '" . $shop . "'");
+ $this->queue->deleteUnnecessary($file_id, $shop);
+ Tools::redirectAdmin($this->context->link->getAdminLink('AdminImport_Api&type=import&file_id=' . $file_id . '&token=' . Tools::getAdminTokenLite('AdminImport_Api'), false));
+
+ }
+
+
+ }
+
+ public function my_translation($string) {
+ return $this->module->l($string);
+ }
+}
\ No newline at end of file
diff --git a/modules/import_api/controllers/front/ajax.php b/modules/import_api/controllers/front/ajax.php
new file mode 100644
index 00000000..350da75f
--- /dev/null
+++ b/modules/import_api/controllers/front/ajax.php
@@ -0,0 +1,106 @@
+
+ * @copyright 2019 Dalibor Stojcevski
+ * @license Dalibor Stojcevski
+ */
+
+class Import_ApiAjaxModuleFrontController extends ModuleFrontController
+{
+
+ /**
+ * Assign template vars related to page content
+ * @see FrontController::initContent()
+ */
+ private $fields = array();
+
+ public function initContent()
+ {
+ $response = $this->find_fields();
+ $json = Tools::jsonEncode($response);
+ $this->ajaxDie($json);
+
+ }
+
+
+ public function find_fields()
+ {
+
+ Db::getInstance()->execute("SET session wait_timeout=28800", FALSE);
+ Db::getInstance()->execute("SET session net_read_timeout=28800", FALSE);
+ Db::getInstance()->execute("SET session interactive_timeout=28800", FALSE);
+ $file_id = Tools::getValue('file_id');
+ $shop = 'default';
+ $json['error'] = '';
+ $php_array = array();
+ $query_file = Db::getInstance()->executeS("SELECT * FROM ". _DB_PREFIX_ . "ia_files WHERE file_id = '" . (int)$file_id . "' AND shop = '" . pSQL($shop) . "' LIMIT 1");
+
+ //$type = Tools::getValue('type');
+ //$link = html_entity_decode($this->request->post['import_api_link'], ENT_QUOTES, "UTF-8");
+
+ if ($query_file) {
+ require_once(_PS_MODULE_DIR_ . 'import_api/classes/filereader.php');
+
+ $FileReader = new FileReader();
+ $file = $query_file[0];
+ //$file['link'] = html_entity_decode($file['link'], ENT_QUOTES, "UTF-8");
+
+ list($php_array, $json['error']) = $FileReader->getArrayFromLink($file);
+
+ if($php_array === null){
+ $json['error'] = 'File is not in selected format';
+ }
+ } else {
+ $json['error'] = 'File not found';
+ }
+
+ if (!$json['error']){
+ if (count($php_array) > 10 && !empty(array_key_first($php_array))) { // if is not 0
+ $new_array = array();
+ foreach ($php_array as $key => $value ) {
+ $value['GENERATED_UNIQUE'] = $key;
+ $new_array[] = $value;
+ }
+ $php_array = $new_array;
+ unset($new_array);
+ }
+ $this->search_keys($php_array, 'FEED');
+ $json['field'] = $this->fields;
+ $json['fields'] = implode('##', $this->fields);
+ //$json['mapping'] = implode('##', $this->fields);
+ if ($this->fields) {
+ Db::getInstance()->execute("UPDATE ". _DB_PREFIX_ . "ia_files SET fields = '" . pSQL($json['fields']) . "', date_edited = '" . time() . "' WHERE file_id = " . (int)$file_id ." AND shop = '" . pSQL($shop) . "'");
+ }
+ }
+
+ return $json;
+ }
+
+ function search_keys($array, $parent){
+ $total = count($array);
+ foreach($array as $key => $value){
+ if(!is_array($value)){
+ if (is_int($key)) {
+ $path = $parent;
+ } else {
+ $path = $parent .'->'. $key;
+ }
+
+ $path = str_replace('->array()', '',$path);
+ if (!in_array($path, $this->fields))
+ $this->fields[] = $path;
+ } else {
+ if (is_int($key)) {
+ $key = "array()";
+ }
+ $this->search_keys($value, $parent .'->'. $key);
+ }
+ }
+ }
+}
diff --git a/modules/import_api/controllers/front/import.php b/modules/import_api/controllers/front/import.php
new file mode 100644
index 00000000..a9d40dd7
--- /dev/null
+++ b/modules/import_api/controllers/front/import.php
@@ -0,0 +1,690 @@
+
+ * @copyright 2019 Dalibor Stojcevski
+ * @license Dalibor Stojcevski
+ */
+ini_set('memory_limit', '-1');
+class Import_ApiImportModuleFrontController extends ModuleFrontController
+{
+
+ /**
+ * Assign template vars related to page content
+ * @see FrontController::initContent()
+ */
+ private $fields = array();
+ private $product_links = array();
+ private $import;
+ private $creator;
+ private $convertor;
+ public static $update_limit = 1;
+
+ public function initContent()
+ {
+
+ require_once(_PS_MODULE_DIR_ . 'import_api/classes/import.php');
+ require_once(_PS_MODULE_DIR_ . 'import_api/classes/creator.php');
+ require_once(_PS_MODULE_DIR_ . 'import_api/classes/filereader.php');
+ require_once(_PS_MODULE_DIR_ . 'import_api/classes/queue.php');
+ require_once(_PS_MODULE_DIR_ . 'import_api/classes/convertor.php');
+ Db::getInstance()->execute("SET session wait_timeout=28800", FALSE);
+ Db::getInstance()->execute("SET session net_read_timeout=28800", FALSE);
+ Db::getInstance()->execute("SET session interactive_timeout=28800", FALSE);
+
+ $json = array();
+
+ $view = Tools::getValue('id');
+
+ $file_id = Tools::getValue('file_id');
+
+ $this->queue = new Queue();
+
+ $this->creator = new Creator($file_id);
+
+ $action = Tools::getValue('action') ? Tools::getValue('action') : 'cron';
+
+ /* if ($this->product_links === null) {
+ $json['error'] = 'Invalid source url. Please check in browser first';
+ $this->ajaxDie(Tools::jsonEncode($json));
+ }*/
+
+ $this->getModuleSettings();
+
+
+ $this->convertor = new Convertor($this->settings, $this->creator);
+
+ if ($action == 'test')
+ {
+ //$this->creator->addAttributesToProduct(array('0' => array('0' => array('attribute'=> "DColor", 'attribute_value' => "blue"), '1' => array('attribute'=> "DColor", 'attribute_value' => "green"))), 12685); exit;
+
+ $this->product_links = $this->getProductLinks($action);
+ $this->printTestJsonData($view);
+ }
+ elseif (Tools::getValue('queue'))
+ {
+ $this->queue($file_id);
+ echo 'koniec';
+ exit;
+ }
+ elseif ($action == 'selected')
+ {
+ $this->importSelected($file_id);
+ }
+ elseif ($action == 'delete')
+ {
+ $this->deleteProducts($file_id);
+ }
+ elseif ($action == 'view_one')
+ {
+ $this->viewOne($file_id, Tools::getValue('index'));
+ }
+ elseif (Tools::getValue('index'))
+ {
+ $this->importOne($file_id, Tools::getValue('index'));
+ }
+ else
+ {
+ $this->importData($file_id, $action, Tools::getValue( 'limit' ) );
+ Tools::clearSmartyCache();
+ Tools::clearXMLCache();
+ Media::clearCache();
+ Tools::generateIndex();
+ }
+ }
+
+ function queue($file_id)
+ {
+ $product_links = $this->getProductLinks();
+
+ $products = array();
+
+ foreach ($product_links as $link)
+ {
+ $original_product = $this->importer->getAllData($link);
+ $original_product = $this->convertor->unArray($original_product);
+ $original_product = $this->convertor->replace($original_product);
+ $original_product = $this->convertor->filter($original_product);
+
+ $original_product = $this->convertor->clearInput($original_product);
+ if ($original_product['belong'])
+ {
+ $products[$original_product['unique']] = $original_product;
+ }
+ }
+
+ $this->queue->saveTempToDb($products, $file_id);
+
+ return count($products);
+ }
+
+ function importData($file_id, $action = 'cron', $limit = 0 )
+ {
+ $json['notice'] = '';
+ $last_queue = $this->queue->getLastQueue($file_id);
+
+ if (!$last_queue || $last_queue['status'] == 4)
+ {
+ // $this->queue($file_id);
+ // $last_queue = $this->queue->getLastQueue($file_id);
+ }
+
+ if (!$last_queue)
+ {
+ exit('Queue creation error');
+ }
+ if ($last_queue['status'] == 3)
+ {
+ $this->creator->processMissing($file_id, $last_queue['queue_id']);
+ $this->ajaxDie(Tools::jsonEncode($json));
+ exit;
+ }
+
+ $products = $this->queue->getQueued($file_id);
+
+ if (!$products)
+ {
+ // $this->queue($file_id);
+ // $products = $this->queue->getQueued($file_id);
+ // $last_queue = $this->queue->getLastQueue($file_id);
+ echo 'koniec';
+ exit;
+ }
+
+
+ $queue_id = isset($products[0]) ? $products[0]['queue_id'] : 0;
+ $products_created = 0;
+ $products_updated = 0;
+
+ $json['total'] = count($products);
+
+ foreach ($products as $product)
+ {
+ $original_product = json_decode($product['product'], true);
+
+ $product_id = 0;
+
+ if ($this->settings['unique_equivalent'] == 'id_product')
+ {
+ $product_id = (int)$original_product['unique'];
+ }
+ else
+ {
+ $equivalent = $this->settings['unique_equivalent'];
+
+ $query = ($equivalent == 'name') ? $original_product['name'] : $original_product['unique'];
+
+ $product_id = $this->creator->getProductId($query, $this->settings['table_equivalent'], $equivalent);
+ }
+
+ $this->queue->deleteQueuedProduct($product['product_id']);
+ if ($product_id)
+ {
+ $this->creator->editProduct($product_id, $original_product);
+ Db::getInstance()->execute("UPDATE " . _DB_PREFIX_ . "ia_products SET indx = '" . pSQL($original_product['unique']) . "', product = '" . pSQL(json_encode($original_product)) . "', source = 'admin', date_edited = '" . time() . "', queue_id = '" . (int)$queue_id . "', file_id = '" . (int)$file_id . "' WHERE product_id = '" . (int)$product_id . "' AND shop = 'default'");
+
+ $products_updated++;
+
+ if ( $action == 'cron' )
+ echo '
Zaktualizowałem produkt: ' . $product_id . '
';
+ }
+ elseif (empty($this->settings['import_api_settings']['only_update']))
+ {
+ $frm_product = $this->convertor->convertToFrmProduct($original_product, $file_id);
+ $product_id = $frm_product->id;
+ Db::getInstance()->execute("INSERT INTO " . _DB_PREFIX_ . "ia_products SET product_id = '" . $product_id . "', indx = '" . pSQL($original_product['unique']) . "', product = '" . pSQL(json_encode($original_product)) . "', shop = 'default', source = 'admin', date_added = '" . time() . "', queue_id = '" . (int)$queue_id . "', file_id = '" . (int)$file_id . "'");
+
+ $products_created++;
+
+ if ( $action == 'cron' )
+ echo 'Dodałem nowy produkt: ' . $product_id . '
';
+ }
+
+ if ( ( ( $products_created + $products_updated ) >= self::$update_limit ) or ( $limit and ( $products_created + $products_updated ) >= $limit ) )
+ {
+ $json['notice'] = 'time_out';
+ break;
+ }
+ }
+
+ if (!$json['notice'])
+ {
+ $json['notice'] = 'missing';
+ Db::getInstance()->execute("UPDATE " . _DB_PREFIX_ . "ia_queues SET source = 'admin', status = '3', date_processed = '" . time() . "' WHERE queue_id = '" . (int)$queue_id . "'");
+ }
+
+ if ($action != 'cron')
+ {
+ $json['products_created'] = $products_created;
+ $json['products_updated'] = $products_updated;
+ $this->ajaxDie(Tools::jsonEncode($json));
+ }
+
+ exit;
+ }
+
+ function importSelected($file_id)
+ {
+ $products = array();
+ $selected = Tools::getValue('indx') ? Tools::getValue('indx') : array();
+
+ foreach ($selected as $indx)
+ {
+ $products[] = $this->queue->getQueuedProduct($indx, $file_id);
+ }
+ $queue_id = isset($products[0]) ? $products[0]['queue_id'] : 0;
+
+ $products_created = 0;
+ $products_updated = 0;
+
+ $json['notice'] = '';
+ $json['total'] = count($products);
+ $json['processed'] = array();
+
+ foreach ($products as $product)
+ {
+ $original_product = json_decode($product['product'], true);
+
+ $json['processed'][] = $product['product_id'];
+
+ $product_id = 0;
+
+ if ($this->settings['unique_equivalent'] == 'id_product')
+ {
+ $product_id = (int)$original_product['unique'];
+ }
+ else
+ {
+ $equivalent = $this->settings['unique_equivalent'];
+
+ $query = ($equivalent == 'name') ? $original_product['name'] : $original_product['unique'];
+
+ $product_id = $this->creator->getProductId($query, $this->settings['table_equivalent'], $equivalent);
+ }
+
+ $this->queue->deleteQueuedProduct($product['product_id']);
+
+ if ($product_id)
+ {
+ $this->creator->editProduct($product_id, $original_product);
+ Db::getInstance()->execute("UPDATE " . _DB_PREFIX_ . "ia_products SET indx = '" . pSQL($indx) . "', product = '" . pSQL(json_encode($original_product)) . "', source = 'selected', date_edited = '" . time() . "', queue_id = '" . (int)$queue_id . "', file_id = '" . (int)$file_id . "' WHERE product_id = '" . (int)$product_id . "' AND shop = 'default'");
+
+ $products_updated++;
+ }
+ elseif (empty($this->settings['import_api_settings']['only_update']))
+ {
+ $frm_product = $this->convertor->convertToFrmProduct($original_product, $file_id);
+ $product_id = $frm_product->id;
+ Db::getInstance()->execute("INSERT INTO " . _DB_PREFIX_ . "ia_products SET product_id = '" . (int)$product_id . "', indx = '" . pSQL($indx) . "', product = '" . pSQL(json_encode($original_product)) . "', shop = 'default', source = 'selected', date_added = '" . time() . "', queue_id = '" . (int)$queue_id . "', file_id = '" . (int)$file_id . "'");
+
+ $products_created++;
+ }
+
+ if ( ( $products_created + $products_updated ) >= self::$update_limit )
+ {
+ $json['notice'] = 'time_out';
+ break;
+ }
+ }
+ $json['products_created'] = $products_created;
+ $json['products_updated'] = $products_updated;
+ $this->ajaxDie(Tools::jsonEncode($json));
+
+ exit;
+ }
+
+ function importOne($file_id, $indx)
+ {
+ $product = $this->queue->getQueuedProduct($indx, $file_id);
+
+ if (!$product)
+ {
+ echo 'Product not found in queue';
+ exit;
+ }
+ $queue_id = isset($product[0]) ? $product['queue_id'] : 0;
+
+
+ $original_product = json_decode($product['product'], true);
+
+ $equivalent = $this->settings['unique_equivalent'];
+
+ $query = ($equivalent == 'name') ? $original_product['name'] : $original_product['unique'];
+
+ $product_id = $this->creator->getProductId($query, $this->settings['table_equivalent'], $equivalent);
+ $link = new Link();
+ if ($product_id)
+ {
+ $exising = $this->creator->editProduct($product_id, $original_product);
+ Db::getInstance()->execute("UPDATE " . _DB_PREFIX_ . "ia_products SET indx = '" . pSQL($indx) . "', product = '" . pSQL(json_encode($original_product)) . "', source = 'selected', date_edited = '" . time() . "', queue_id = '" . (int)$queue_id . "', file_id = '" . (int)$file_id . "' WHERE product_id = '" . (int)$product_id . "' AND shop = 'default'");
+ echo 'Product is updated';
+ //echo 'link to product ' . $link->getproductlink($product_id) . ' ';
+ }
+ else
+ {
+ $frm_product = $this->convertor->convertToFrmProduct($original_product, $file_id);
+ if ($frm_product)
+ {
+ $product_id = $frm_product->id;
+ Db::getInstance()->execute("INSERT INTO " . _DB_PREFIX_ . "ia_products SET product_id = '" . (int)$product_id . "', indx = '" . pSQL($indx) . "', product = '" . pSQL(json_encode($original_product)) . "', shop = 'default', source = 'selected', date_added = '" . time() . "', queue_id = '" . (int)$queue_id . "', file_id = '" . (int)$file_id . "'");
+ echo 'Product is inserted';
+ //echo 'Link to product ' . $link->getProductLink($product_id) . ' ';
+
+ }
+ }
+
+ exit;
+ }
+
+ function viewOne($file_id, $indx)
+ {
+ $product = $this->queue->getQueuedProduct($indx, $file_id);
+
+ if (!$product)
+ {
+ echo 'Product not found in queue';
+ exit;
+ }
+
+ $queue_id = isset($product[0]) ? $product['queue_id'] : 0;
+
+ $original_product = json_decode($product['product'], true);
+
+ $equivalent = $this->settings['unique_equivalent'];
+
+ $query = ($equivalent == 'name') ? $original_product['name'] : $original_product['unique'];
+
+ $product_id = $this->creator->getProductId($query, $this->settings['table_equivalent'], $equivalent);
+ $link = new Link();
+ if ($product_id)
+ {
+ echo 'Product exists ' . $product_id;
+ //echo 'link to product ' . $link->getproductlink($exising) . ' ';
+ }
+ else
+ {
+
+ echo 'Product is not inserted in Prestashop site ';
+ }
+
+ foreach ($original_product as $key => $value)
+ {
+ if (!is_array($value))
+ {
+ echo $key . ':' . $value . ', ';
+ }
+ }
+
+ if (isset($original_product['cover']))
+ {
+ if (!$this->UR_exists($original_product['cover']))
+ {
+ echo 'image is not found. If you need to add server name go to Modifications and add https://servername/[[cover]]';
+ }
+ else
+ {
+ echo 'image: ' . $original_product['cover'];
+ }
+ }
+ exit;
+ }
+
+ private function printTestJsonData($view)
+ {
+ $json = array();
+
+ foreach ($this->product_links as $link)
+ {
+ $json[] = $this->importer->getAllData($link, $view);
+ }
+
+ $this->ajaxDie(Tools::jsonEncode($json));
+ }
+
+ private function getProductLinks($action = 'import')
+ {
+ $begin_character = 'FEED';
+
+ $shop = 'default';
+
+ $file_id = Tools::getValue('file_id');
+
+ $settings = $this->getModuleSettings($file_id);
+
+ $query_file = Db::getInstance()->executeS("SELECT * FROM " . _DB_PREFIX_ . "ia_files WHERE file_id = '" . (int)$file_id . "' AND shop = '" . pSQL($shop) . "' LIMIT 1");
+
+ $file = $query_file[0];
+
+ $FileReader = new FileReader();
+
+ list($tmp_array, $error) = $FileReader->getArrayFromLink($file);
+
+ if ( is_array( $tmp_array) and count($tmp_array) > 10 && !empty(array_key_first($tmp_array)))
+ { // if is not 0
+ $new_array = array();
+ foreach ($tmp_array as $key => $value)
+ {
+ $value['GENERATED_UNIQUE'] = $key;
+ $new_array[] = $value;
+ }
+ $tmp_array = $new_array;
+ unset($new_array);
+ }
+
+ $php_array[$begin_character] = $tmp_array;
+
+ $parts = explode('->', $settings['unique_field']);
+
+ $identifier_field = $parts[count($parts) - 1];
+
+
+ $importer = new Import($settings);
+
+ $importer->setJsonArray($php_array);
+ $importer->findUniqueProductsIdentifier($php_array, $parts, $identifier_field);
+
+
+ $product_links = $importer->getProductLinks();
+
+ //var_dump($product_links); exit;
+
+ $this->importer = $importer;
+
+ $start = $limit = 0;
+
+ if ($action == 'test')
+ {
+ $start = $settings['start'];
+ $limit = 20;
+ }
+
+ if (Tools::getValue('start'))
+ {
+ $start = Tools::getValue('start');
+ }
+
+ if (Tools::getValue('limit'))
+ {
+ $limit = Tools::getValue('limit');
+ }
+
+ if ($start && !$limit)
+ {
+ $limit = count($product_links) - $start;
+ }
+
+ if ($limit)
+ {
+ $product_links = array_slice($product_links, $start, $limit);
+ }
+ return $product_links;
+ }
+
+ function getModuleSettings($file_id = 0)
+ {
+
+ if (!$file_id)
+ {
+ $file_id = Tools::getValue('file_id');
+ }
+
+ $shop = 'default';
+
+ $settings_fields = array('link', 'attribute_group', 'default_brand', 'default_category', 'top_category', 'weight_class_id', 'stock_status_id', 'tax', 'default_option', 'category_path', 'multiplier', 'id_tax_rules_group');
+
+ if (Tools::getValue('import_api_field'))
+ {
+
+ $settings = array(
+ 'import_api_field' => Tools::getValue('import_api_field'),
+ 'import_api_modification' => Tools::getValue('import_api_modification'),
+ 'import_api_combination' => Tools::getValue('import_api_combination'),
+ 'import_api_settings' => Tools::getValue('import_api_settings'),
+ 'import_api_link' => Tools::getValue('import_api_link'),
+ 'import_api_type' => Tools::getValue('import_api_type'),
+ );
+ }
+ else
+ {
+ $query_file_settings = Db::getInstance()->executeS("SELECT fs.* FROM " . _DB_PREFIX_ . "ia_file_settings fs WHERE fs.file_id = '" . (int)$file_id . "' LIMIT 1");
+ if ($query_file_settings)
+ {
+ $file_settings = $query_file_settings[0];
+ $filters = array();
+ $replaces = array();
+ if ($file_settings['filter'])
+ {
+ $text_areas = json_decode($file_settings['filter'], true);
+ foreach ($text_areas as $key => $textarea_value)
+ {
+ if ($textarea_value)
+ {
+ $filters[$key] = array_map('trim', explode("\n", $textarea_value));
+ }
+ }
+ }
+ if ($file_settings['replace'])
+ {
+ $text_areas = json_decode($file_settings['replace'], true);
+ foreach ($text_areas as $key => $textarea_value)
+ {
+ $lines = array_map('trim', explode("\n", $textarea_value));
+ foreach ($lines as $line)
+ {
+ if ($line)
+ {
+ $replaces[$key][] = explode('##', $line);
+ }
+ }
+ }
+ }
+
+ $settings = array(
+ 'import_api_field' => json_decode($file_settings['mapping'], true),
+ 'import_api_modification' => json_decode($file_settings['modification'], true),
+ 'import_api_combination' => json_decode($file_settings['split'], true),
+ 'import_api_filter' => $filters,
+ 'import_api_replace' => $replaces,
+ 'import_api_filter_options' => json_decode($file_settings['filter_options'], true),
+ 'import_api_settings' => json_decode($file_settings['settings'], true),
+ );
+ }
+ }
+
+ if (!empty($settings['import_api_field']['unique']))
+ {
+ $settings['unique_field'] = html_entity_decode($settings['import_api_field']['unique'], ENT_QUOTES, 'UTF-8');
+ }
+ else
+ {
+ $json['error'] = 'you need to set unique field';
+ $this->ajaxDie(Tools::jsonEncode($json));
+ }
+
+ if (Tools::getValue('start'))
+ {
+ $settings['start'] = Tools::getValue('start');
+ }
+ else
+ {
+ $settings['start'] = 0;
+ }
+
+ if ($settings['import_api_settings']['top_category'])
+ {
+ $settings['top_category_id'] = $this->creator->getCategoryId($settings['import_api_settings']['top_category']);
+ }
+ else
+ {
+ $settings['top_category_id'] = Configuration::get('PS_HOME_CATEGORY');
+ }
+
+ if ($settings['import_api_settings']['default_category'])
+ {
+ $settings['default_category_id'] = $this->creator->getCategoryId($settings['import_api_settings']['default_category']);
+ }
+ else
+ {
+ $settings['default_category_id'] = 0;
+ }
+
+ if ($settings['import_api_settings']['default_brand'])
+ {
+ $settings['default_manufacturer_id'] = $this->creator->getManufacturerId($settings['import_api_settings']['default_brand']);
+ }
+ else
+ {
+ $settings['default_manufacturer_id'] = 0;
+ }
+
+ $settings['id_tax_rules_group'] = isset($settings['import_api_settings']['id_tax_rules_group']) ? $settings['import_api_settings']['id_tax_rules_group'] : -1;
+
+ $settings['unique_equivalent'] = 'name';
+ $settings['table_equivalent'] = 'product_lang';
+ if (isset($settings['import_api_settings']['synchronize_field']) && $settings['import_api_settings']['synchronize_field'] != 'automatic')
+ {
+ $settings['unique_equivalent'] = $settings['import_api_settings']['synchronize_field'];
+ }
+ else
+ {
+ $possible_eq = ['reference', 'sku', 'ean13', 'upc'];
+
+ foreach ($possible_eq as $f)
+ {
+ if (!empty($settings['import_api_field'][$f]) && $settings['import_api_field'][$f] == $settings['import_api_field']['unique'])
+ {
+ $settings['unique_equivalent'] = $f;
+ break;
+ }
+ }
+ }
+ if ($settings['unique_equivalent'] != 'name')
+ {
+ $settings['table_equivalent'] = 'product';
+ }
+
+ $this->settings = $settings;
+
+ return $settings;
+ }
+
+ function deleteProducts($file_id)
+ {
+ $query_file = Db::getInstance()->executeS("SELECT * FROM " . _DB_PREFIX_ . "ia_files WHERE file_id = '" . (int)$file_id . "' LIMIT 1");
+ if (empty($query_file))
+ {
+ $json['notice'] = 'File not found';
+ $this->ajaxDie(Tools::jsonEncode($json));
+ exit;
+ }
+ else
+ {
+ $file = $query_file[0];
+ }
+ if ($file['mask'] . $file['date_added'] != Tools::getValue('token'))
+ {
+ $json['notice'] = 'File not found.....';
+ $this->ajaxDie(Tools::jsonEncode($json));
+ exit;
+ }
+
+ $max_execution_time = 50;
+
+ $products = $this->queue->getProducts($file_id);
+
+ $products_deleted = 0;
+
+ $json['notice'] = '';
+
+ foreach ($products as $product)
+ {
+ $p = new Product($product['product_id']);
+ $p->delete();
+ $this->queue->deleteProduct($product['product_id']);
+ $products_deleted++;
+
+ if ( ( $products_created + $products_updated ) >= self::$update_limit )
+ {
+ $json['notice'] = 'time_out';
+ break;
+ }
+ }
+
+ $json['products_deleted'] = $products_deleted;
+ $this->ajaxDie(Tools::jsonEncode($json));
+
+ exit;
+ }
+
+
+ function UR_exists($url)
+ {
+ $headers = @get_headers($url);
+ return stripos($headers[0], "200 OK") ? true : false;
+ }
+}
diff --git a/modules/import_api/controllers/front/index.php b/modules/import_api/controllers/front/index.php
new file mode 100644
index 00000000..8761a003
--- /dev/null
+++ b/modules/import_api/controllers/front/index.php
@@ -0,0 +1,35 @@
+
+* @copyright 2007-2016 PrestaShop SA
+* @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0)
+* International Registered Trademark & Property of PrestaShop SA
+*/
+
+header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
+header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT');
+
+header('Cache-Control: no-store, no-cache, must-revalidate');
+header('Cache-Control: post-check=0, pre-check=0', false);
+header('Pragma: no-cache');
+
+header('Location: ../');
+exit;
\ No newline at end of file
diff --git a/modules/import_api/controllers/index.php b/modules/import_api/controllers/index.php
new file mode 100644
index 00000000..8761a003
--- /dev/null
+++ b/modules/import_api/controllers/index.php
@@ -0,0 +1,35 @@
+
+* @copyright 2007-2016 PrestaShop SA
+* @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0)
+* International Registered Trademark & Property of PrestaShop SA
+*/
+
+header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
+header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT');
+
+header('Cache-Control: no-store, no-cache, must-revalidate');
+header('Cache-Control: post-check=0, pre-check=0', false);
+header('Pragma: no-cache');
+
+header('Location: ../');
+exit;
\ No newline at end of file
diff --git a/modules/import_api/import_api.php b/modules/import_api/import_api.php
new file mode 100644
index 00000000..fe55dddb
--- /dev/null
+++ b/modules/import_api/import_api.php
@@ -0,0 +1,102 @@
+
+ * @copyright 2019 Dalibor Stojcevski
+ * @license Dalibor Stojcevski
+ */
+
+if (!defined('_PS_VERSION_')) {
+ exit;
+}
+
+class import_api extends Module
+{
+ protected $config_form = false;
+
+ protected $_postErrors = array();
+
+ protected $fmr_fields;
+
+
+ public function __construct()
+ {
+ $this->name = 'import_api';
+ $this->tab = 'front_office_features';
+ $this->version = '3.1.5.9';
+ $this->author = 'Dalibor Stojcevski';
+ $this->need_instance = 1;
+ $this->bootstrap = true;
+ $this->module_key = '3711c016ea39f2f2aa38850e48aa7455';
+ parent::__construct();
+ $this->displayName = $this->l('Import Api');
+ $this->description = $this->l('Import and update products from external source');
+ $this->ps_versions_compliancy = array('min' => '1.6', 'max' => _PS_VERSION_);
+
+ $this->frm_fields = ['unique', 'reference', 'name', 'description', 'price', 'quantity', 'images', 'cover', 'brand', 'category', 'category_parent', 'ean13', 'upc', 'condition', 'weight', 'feature', 'feature_value'];
+ $this->frm_settings = array('top_category' => '', 'default_category' => '', 'default_brand' => '', 'price_multiplier' => '', 'category_path' => 0);
+ }
+
+ public function install()
+ {
+ $class = 'AdminImport_Api';
+ $tab = new Tab();
+ $tab->class_name = $class;
+ $tab->module = $this->name;
+ $tab->id_parent = 0;
+ $langs = Language::getLanguages(false);
+ foreach ($langs as $l) {
+ $tab->name[$l['id_lang']] = $this->l('Import Products');
+ }
+ $tab->save();
+ return $this->installDb() && parent::install();
+ }
+
+ public function uninstall()
+ {
+ return parent::uninstall();
+ }
+
+ public function installDb()
+ {
+ $return = true;
+ require_once(_PS_MODULE_DIR_ . 'import_api/sql_install.php');
+
+ foreach ($sql as $s) {
+ Db::getInstance()->execute($s);
+ }
+
+ $res = Db::getInstance()->executeS("SHOW columns FROM `"._DB_PREFIX_."ia_files` LIKE 'status'");
+ if (!$res) {
+ Db::getInstance()->execute('ALTER TABLE `'._DB_PREFIX_.'ia_files` ADD `status` SMALLINT(2) NOT NULL DEFAULT \'1\'');
+ }
+
+ $res_1 = Db::getInstance()->executeS("SHOW columns FROM `"._DB_PREFIX_."ia_file_settings` LIKE 'replace' ");
+ if (!$res_1) {
+ Db::getInstance()->execute('ALTER TABLE `'._DB_PREFIX_.'ia_file_settings` ADD `replace` text NOT NULL');
+ }
+
+ $res_2 = Db::getInstance()->executeS("SHOW columns FROM `"._DB_PREFIX_."ia_file_settings` LIKE 'filter' ");
+ if (!$res_2) {
+ Db::getInstance()->execute('ALTER TABLE `'._DB_PREFIX_.'ia_file_settings` ADD `filter` text NOT NULL');
+ }
+
+ $res_3 = Db::getInstance()->executeS("SHOW columns FROM `"._DB_PREFIX_."ia_file_settings` LIKE 'filter_options' ");
+
+ if (!$res_3) {
+ Db::getInstance()->execute('ALTER TABLE `'._DB_PREFIX_.'ia_file_settings` ADD `filter_options` text NOT NULL');
+ }
+ return $return;
+ }
+
+ public function getContent()
+ {
+ Tools::redirectAdmin($this->context->link->getAdminLink('AdminImport_Api'));
+ }
+
+}
\ No newline at end of file
diff --git a/modules/import_api/index.php b/modules/import_api/index.php
new file mode 100644
index 00000000..907720c1
--- /dev/null
+++ b/modules/import_api/index.php
@@ -0,0 +1,35 @@
+
+* @copyright 2007-2018 PrestaShop SA
+* @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0)
+* International Registered Trademark & Property of PrestaShop SA
+*/
+
+header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
+header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT');
+
+header('Cache-Control: no-store, no-cache, must-revalidate');
+header('Cache-Control: post-check=0, pre-check=0', false);
+header('Pragma: no-cache');
+
+header('Location: ../');
+exit;
diff --git a/modules/import_api/lib/SimpleXLS.php b/modules/import_api/lib/SimpleXLS.php
new file mode 100644
index 00000000..f6d8c20b
--- /dev/null
+++ b/modules/import_api/lib/SimpleXLS.php
@@ -0,0 +1,1224 @@
+';
+
+if ( $xls = SimpleXLS::parse('excel5book.xls')) {
+ print_r( $xls->rows() ); // dump first sheet
+ print_r( $xls->rows(1)); /// dump second sheet
+} else {
+ echo SimpleXLSX::parseError();
+}
+
+echo '';
+ */
+
+/**
+ * A class for reading Microsoft Excel Spreadsheets.
+ *
+ * SimpleXLS version 2016-2020 packaged by Sergey Shuchkin from
+ * Spreadsheet_Excel_Reader class developed by Vadim Tkachenko
+ */
+class SimpleXLS {
+ const BIFF8 = 0x600;
+ const BIFF7 = 0x500;
+ const WORKBOOKGLOBALS = 0x5;
+ const WORKSHEET = 0x10;
+
+ //const TYPE_BOF = 0x809;
+ const TYPE_EOF = 0x0a;
+ const TYPE_BOUNDSHEET = 0x85;
+ const TYPE_DIMENSION = 0x200;
+ const TYPE_ROW = 0x208;
+ const TYPE_DBCELL = 0xd7;
+ const TYPE_FILEPASS = 0x2f;
+ //const TYPE_NOTE = 0x1c;
+ //const TYPE_TXO = 0x1b6;
+ const TYPE_RK = 0x7e;
+ const TYPE_RK2 = 0x27e;
+ const TYPE_MULRK = 0xbd;
+ const TYPE_MULBLANK = 0xbe;
+ //const TYPE_INDEX = 0x20b;
+ const TYPE_SST = 0xfc;
+ //const TYPE_EXTSST = 0xff;
+ //const TYPE_CONTINUE = 0x3c;
+ const TYPE_LABEL = 0x204;
+ const TYPE_LABELSST = 0xfd;
+ const TYPE_NUMBER = 0x203;
+ const TYPE_NAME = 0x18;
+ //const TYPE_ARRAY = 0x221;
+ //const TYPE_STRING = 0x207;
+ const TYPE_FORMULA = 0x406;
+ const TYPE_FORMULA2 = 0x6;
+ const TYPE_FORMAT = 0x41e;
+ const TYPE_XF = 0xe0;
+ const TYPE_BOOLERR = 0x205;
+ //const TYPE_UNKNOWN = 0xffff;
+ const TYPE_NINETEENFOUR = 0x22;
+ const TYPE_MERGEDCELLS = 0xE5;
+
+ //const DEF_NUM_FORMAT = "%.2f";
+ const DEF_NUM_FORMAT = '%s';
+
+ // OLE
+ const NUM_BIG_BLOCK_DEPOT_BLOCKS_POS = 0x2c;
+ const SMALL_BLOCK_DEPOT_BLOCK_POS = 0x3c;
+ const ROOT_START_BLOCK_POS = 0x30;
+ const BIG_BLOCK_SIZE = 0x200;
+ const SMALL_BLOCK_SIZE = 0x40;
+ const EXTENSION_BLOCK_POS = 0x44;
+ const NUM_EXTENSION_BLOCK_POS = 0x48;
+ const PROPERTY_STORAGE_BLOCK_SIZE = 0x80;
+ const BIG_BLOCK_DEPOT_BLOCKS_POS = 0x4c;
+ const SMALL_BLOCK_THRESHOLD = 0x1000;
+ // property storage offsets
+ const SIZE_OF_NAME_POS = 0x40;
+ const TYPE_POS = 0x42;
+ const START_BLOCK_POS = 0x74;
+ const SIZE_POS = 0x78;
+ /**
+ * Array of worksheets found
+ *
+ * @var array
+ * @access public
+ */
+ public $boundsheets = array();
+
+ /**
+ * Array of format records found
+ *
+ * @var array
+ * @access public
+ */
+ public $formatRecords = array();
+
+ /**
+ *
+ * @var array
+ * @access public
+ */
+ public $sst = array();
+
+ /**
+ * Array of worksheets
+ *
+ * The data is stored in 'cells' and the meta-data is stored in an array
+ * called 'cellsInfo'
+ *
+ * Example:
+ *
+ * $sheets --> 'cells' --> row --> column --> Interpreted value
+ * --> 'cellsInfo' --> row --> column --> 'type' - Can be 'date', 'number', or 'unknown'
+ * --> 'raw' - The raw data that Excel stores for that data cell
+ *
+ * @var array
+ * @access public
+ */
+ public $sheets = array();
+ /**
+ * List of default date formats used by Excel
+ *
+ * @var array
+ * @access public
+ */
+ public $dateFormats = array(
+ 0xe => 'd/m/Y',
+ 0xf => 'd-M-Y',
+ 0x10 => 'd-M',
+ 0x11 => 'M-Y',
+ 0x12 => 'h:i a',
+ 0x13 => 'h:i:s a',
+ 0x14 => 'H:i',
+ 0x15 => 'H:i:s',
+ 0x16 => 'd/m/Y H:i',
+ 0x2d => 'i:s',
+ 0x2e => 'H:i:s',
+ 0x2f => 'i:s.S'
+ );
+/**
+ * Default number formats used by Excel
+ *
+ * @var array
+ * @access public
+ */
+ public $numberFormats = array(
+ 0x1 => '%1.0f', // "0"
+ 0x2 => '%1.2f', // "0.00",
+ 0x3 => '%1.0f', //"#,##0",
+ 0x4 => '%1.2f', //"#,##0.00",
+ 0x5 => '%1.0f', /*"$#,##0;($#,##0)",*/
+ 0x6 => '$%1.0f', /*"$#,##0;($#,##0)",*/
+ 0x7 => '$%1.2f', //"$#,##0.00;($#,##0.00)",
+ 0x8 => '$%1.2f', //"$#,##0.00;($#,##0.00)",
+ 0x9 => '%1.0f%%', // "0%"
+ 0xa => '%1.2f%%', // "0.00%"
+ 0xb => '%1.2f', // 0.00E00",
+ 0x25 => '%1.0f', // "#,##0;(#,##0)",
+ 0x26 => '%1.0f', //"#,##0;(#,##0)",
+ 0x27 => '%1.2f', //"#,##0.00;(#,##0.00)",
+ 0x28 => '%1.2f', //"#,##0.00;(#,##0.00)",
+ 0x29 => '%1.0f', //"#,##0;(#,##0)",
+ 0x2a => '$%1.0f', //"$#,##0;($#,##0)",
+ 0x2b => '%1.2f', //"#,##0.00;(#,##0.00)",
+ 0x2c => '$%1.2f', //"$#,##0.00;($#,##0.00)",
+ 0x30 => '%1.0f'
+ );
+ private $datetimeFormat = 'Y-m-d H:i:s';
+ /**
+ * Default encoding
+ *
+ * @var string
+ * @access private
+ */
+ private $defaultEncoding = 'UTF-8';
+ /**
+ * Default number format
+ *
+ * @var integer
+ * @access private
+ */
+ private $defaultFormat = self::DEF_NUM_FORMAT;
+ /**
+ * List of formats to use for each column
+ *
+ * @var array
+ * @access private
+ */
+ private $columnsFormat = array();
+ /**
+ *
+ * @var integer
+ * @access private
+ */
+ private $rowoffset = 1; //"##0.0E0";
+
+ // }}}
+ /**
+ *
+ * @var integer
+ * @access private
+ */
+ private $colOffset = 1;
+ private $recType;
+ private $nineteenFour;
+ private $multiplier;
+ private $sn;
+ private $curFormat;
+
+ // OLERead
+ private $data;
+ private $numBigBlockDepotBlocks;
+ private $sbdStartBlock;
+ private $rootStartBlock;
+ private $extensionBlock;
+ private $numExtensionBlocks;
+ private $bigBlockChain;
+ private $smallBlockChain;
+ private $rootEntry;
+ private $entry;
+ private $props;
+
+ // sergey.shuchkin@gmail.com
+ private $wrkbook; // false - to use excel format
+ private $error = false;
+ private $debug;
+
+ // {{{ Spreadsheet_Excel_Reader()
+
+ /**
+ * Constructor
+ *
+ * @param string $filename XLS Filename or xls contents
+ * @param bool $isData If True then $filename is contents
+ * @param bool $debug Trigger PHP errors?
+ */
+ public function __construct( $filename, $isData = false, $debug = false ) {
+ $this->debug = $debug;
+ $this->_oleread( $filename, $isData );
+ $this->_parse();
+ }
+ public static function parse( $filename, $isData = false, $debug = false ) {
+ $xlsx = new self( $filename, $isData, $debug );
+ if ( $xlsx->success() ) {
+ return $xlsx;
+ }
+ self::parseError( $xlsx->error() );
+
+ return false;
+ }
+ public static function parseError( $set = false ) {
+ static $error = false;
+ return $set ? $error = $set : $error;
+ }
+ public function error( $set = false ) {
+ if ( $set ) {
+ $this->error = $set;
+ if ( $this->debug ) {
+ trigger_error( $set );
+ }
+ }
+
+ return $this->error;
+ }
+ public function success() {
+ return ! $this->error;
+ }
+ public function rows( $sheetNum = 0 ) {
+ if ( $this->sheets[ $sheetNum ] ) {
+ $s = $this->sheets[ $sheetNum ];
+ $result = array();
+ for ( $i = 0; $i < $s['numRows']; $i ++ ) {
+ $r = array();
+ for ( $j = 0; $j < $s['numCols']; $j ++ ) {
+ $r[ $j ] = isset( $s['cells'][ $i + 1 ][ $j + 1 ] ) ? $s['cells'][ $i + 1 ][ $j + 1 ] : '';
+ }
+ $result[] = $r;
+ }
+
+ return $result;
+ }
+
+ return false;
+ }
+ public function setDateTimeFormat( $value ) {
+ $this->datetimeFormat = is_string( $value) ? $value : false;
+ }
+
+ // }}}
+
+ private function _oleread( $sFileName, $isData = false ) {
+ if ( $isData ) {
+ $this->data = $sFileName;
+ } else {
+ // check if file exist and is readable (Darko Miljanovic)
+ if ( ! is_readable( $sFileName ) ) {
+ $this->error( 'File not is readable ' . $sFileName );
+ return false;
+ }
+
+ $this->data = file_get_contents( $sFileName );
+ if ( ! $this->data ) {
+ $this->error( 'File reading error ' . $sFileName );
+ return false;
+ }
+ }
+ //echo IDENTIFIER_OLE;
+ //echo 'start';
+ if ( $this->_strpos( $this->data, pack( 'CCCCCCCC', 0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1 ) ) !== 0 ) {
+ $this->error( 'File is not XLS' );
+
+ return false;
+ }
+
+ $this->numBigBlockDepotBlocks = $this->_GetInt4d( $this->data, self::NUM_BIG_BLOCK_DEPOT_BLOCKS_POS );
+ $this->sbdStartBlock = $this->_GetInt4d( $this->data, self::SMALL_BLOCK_DEPOT_BLOCK_POS );
+ $this->rootStartBlock = $this->_GetInt4d( $this->data, self::ROOT_START_BLOCK_POS );
+ $this->extensionBlock = $this->_GetInt4d( $this->data, self::EXTENSION_BLOCK_POS );
+ $this->numExtensionBlocks = $this->_GetInt4d( $this->data, self::NUM_EXTENSION_BLOCK_POS );
+
+/*
+ echo $this->numBigBlockDepotBlocks." ";
+ echo $this->sbdStartBlock." ";
+ echo $this->rootStartBlock." ";
+ echo $this->extensionBlock." ";
+ echo $this->numExtensionBlocks." ";
+
+*/
+ //echo "sbdStartBlock = $this->sbdStartBlock\n";
+ $bigBlockDepotBlocks = array();
+ $pos = self::BIG_BLOCK_DEPOT_BLOCKS_POS;
+ // echo "pos = $pos";
+ $bbdBlocks = $this->numBigBlockDepotBlocks;
+
+ if ( $this->numExtensionBlocks !== 0 ) {
+ $bbdBlocks = ( self::BIG_BLOCK_SIZE - self::BIG_BLOCK_DEPOT_BLOCKS_POS ) / 4;
+ }
+
+ for ( $i = 0; $i < $bbdBlocks; $i ++ ) {
+ $bigBlockDepotBlocks[ $i ] = $this->_GetInt4d( $this->data, $pos );
+ $pos += 4;
+ }
+
+
+ for ( $j = 0; $j < $this->numExtensionBlocks; $j ++ ) {
+ $pos = ( $this->extensionBlock + 1 ) * self::BIG_BLOCK_SIZE;
+ $blocksToRead = min( $this->numBigBlockDepotBlocks - $bbdBlocks, self::BIG_BLOCK_SIZE / 4 - 1 );
+
+ for ( $i = $bbdBlocks; $i < $bbdBlocks + $blocksToRead; $i ++ ) {
+ $bigBlockDepotBlocks[ $i ] = $this->_GetInt4d( $this->data, $pos );
+ $pos += 4;
+ }
+
+ $bbdBlocks += $blocksToRead;
+ if ( $bbdBlocks < $this->numBigBlockDepotBlocks ) {
+ $this->extensionBlock = $this->_GetInt4d( $this->data, $pos );
+ }
+ }
+
+ // var_dump($bigBlockDepotBlocks);
+
+ // readBigBlockDepot
+
+ $index = 0;
+ $this->bigBlockChain = array();
+
+ for ( $i = 0; $i < $this->numBigBlockDepotBlocks; $i ++ ) {
+ $pos = ( $bigBlockDepotBlocks[ $i ] + 1 ) * self::BIG_BLOCK_SIZE;
+ //echo "pos = $pos";
+ for ( $j = 0; $j < self::BIG_BLOCK_SIZE / 4; $j ++ ) {
+ $this->bigBlockChain[ $index ] = $this->_GetInt4d( $this->data, $pos );
+ $pos += 4;
+ $index ++;
+ }
+ }
+
+ //var_dump($this->bigBlockChain);
+ //echo '=====2';
+ // readSmallBlockDepot();
+
+ $index = 0;
+ $sbdBlock = $this->sbdStartBlock;
+ $this->smallBlockChain = array();
+
+ while ( $sbdBlock !== - 2 ) {
+
+ $pos = ( $sbdBlock + 1 ) * self::BIG_BLOCK_SIZE;
+
+ for ( $j = 0; $j < self::BIG_BLOCK_SIZE / 4; $j ++ ) {
+ $this->smallBlockChain[ $index ] = $this->_GetInt4d( $this->data, $pos );
+ $pos += 4;
+ $index ++;
+ }
+
+ $sbdBlock = $this->bigBlockChain[ $sbdBlock ];
+ }
+
+
+ // readData(rootStartBlock)
+ $block = $this->rootStartBlock;
+
+ $this->entry = $this->_readData( $block );
+
+ /*
+ while ($block != -2) {
+ $pos = ($block + 1) * self::BIG_BLOCK_SIZE;
+ $this->entry = $this->entry.$this->_substr($this->_data, $pos, self::BIG_BLOCK_SIZE);
+ $block = $this->bigBlockChain[$block];
+ }
+ */
+ //echo '==='.$this->entry."===";
+ $this->_readPropertySets();
+ $this->data = $this->_readWorkBook();
+
+ return true;
+ }
+
+ // {{{ setOutputEncoding()
+
+ public function _GetInt4d( $data, $pos ) {
+ $value = ord( $data[ $pos ] ) | ( ord( $data[ $pos + 1 ] ) << 8 ) | ( ord( $data[ $pos + 2 ] ) << 16 ) | ( ord( $data[ $pos + 3 ] ) << 24 );
+ return ($value > 0x7FFFFFFF) ? $value - 0x100000000 : $value;
+ }
+
+ // }}}
+
+ // {{{ setRowColOffset()
+
+ public function _readData( $bl ) {
+ $block = $bl;
+
+ $data = '';
+
+ while ( $block !== - 2 ) {
+ $pos = ( $block + 1 ) * self::BIG_BLOCK_SIZE;
+ $data .= $this->_substr( $this->data, $pos, self::BIG_BLOCK_SIZE );
+ //echo "pos = $pos data=$data\n";
+ $block = $this->bigBlockChain[ $block ];
+ }
+
+ return $data;
+ }
+
+ // }}}
+ // {{{ setDefaultFormat()
+
+ public function _readPropertySets() {
+ $offset = 0;
+ //var_dump($this->entry);
+ while ( $offset < $this->_strlen( $this->entry ) ) {
+ $d = $this->_substr( $this->entry, $offset, self::PROPERTY_STORAGE_BLOCK_SIZE );
+
+ $nameSize = ord( $d[ self::SIZE_OF_NAME_POS ] ) | ( ord( $d[ self::SIZE_OF_NAME_POS + 1 ] ) << 8 );
+
+ $type = ord( $d[ self::TYPE_POS ] );
+ //$maxBlock = $this->_strlen($d) / self::BIG_BLOCK_SIZE - 1;
+
+ $startBlock = $this->_GetInt4d( $d, self::START_BLOCK_POS );
+ $size = $this->_GetInt4d( $d, self::SIZE_POS );
+
+ $name = '';
+ for ( $i = 0; $i < $nameSize; $i ++ ) {
+ $name .= $d[ $i ];
+ }
+
+ $name = str_replace( "\x00", '', $name );
+
+ $this->props[] = array(
+ 'name' => $name,
+ 'type' => $type,
+ 'startBlock' => $startBlock,
+ 'size' => $size
+ );
+
+ if ( ( $name === 'Workbook' ) || ( $name === 'Book' ) ) {
+ $this->wrkbook = count( $this->props ) - 1;
+ }
+
+ if ( $name === 'Root Entry' ) {
+ $this->rootEntry = count( $this->props ) - 1;
+ }
+
+ //echo "name ==$name=\n";
+
+
+ $offset += self::PROPERTY_STORAGE_BLOCK_SIZE;
+ }
+
+ }
+
+ // }}}
+ // {{{ setColumnFormat()
+
+ private function _readWorkBook() {
+ if ( $this->props[ $this->wrkbook ]['size'] < self::SMALL_BLOCK_THRESHOLD ) {
+// getSmallBlockStream(PropertyStorage ps)
+
+ $rootdata = $this->_readData( $this->props[ $this->rootEntry ]['startBlock'] );
+
+ $streamData = '';
+ $block = (int) $this->props[ $this->wrkbook ]['startBlock'];
+ //$count = 0;
+ while ( $block !== - 2 ) {
+ $pos = $block * self::SMALL_BLOCK_SIZE;
+ $streamData .= $this->_substr( $rootdata, $pos, self::SMALL_BLOCK_SIZE );
+
+ $block = $this->smallBlockChain[ $block ];
+ }
+
+ return $streamData;
+
+
+ }
+
+ $numBlocks = $this->props[ $this->wrkbook ]['size'] / self::BIG_BLOCK_SIZE;
+ if ( $this->props[ $this->wrkbook ]['size'] % self::BIG_BLOCK_SIZE !== 0 ) {
+ $numBlocks ++;
+ }
+
+ if ( $numBlocks === 0 ) {
+ return '';
+ }
+
+ //echo "numBlocks = $numBlocks\n";
+ //byte[] streamData = new byte[numBlocks * self::BIG_BLOCK_SIZE];
+ //print_r($this->wrkbook);
+ $streamData = '';
+ $block = $this->props[ $this->wrkbook ]['startBlock'];
+
+ //echo "block = $block";
+ while ( $block !== - 2 ) {
+ $pos = ( $block + 1 ) * self::BIG_BLOCK_SIZE;
+ $streamData .= $this->_substr( $this->data, $pos, self::BIG_BLOCK_SIZE );
+ $block = $this->bigBlockChain[ $block ];
+ }
+
+ //echo 'stream'.$streamData;
+ return $streamData;
+ }
+
+
+ // }}}
+ // {{{ _parse()
+
+ /**
+ * Parse a workbook
+ *
+ * @access private
+ * @return bool
+ */
+ public function _parse() {
+ $pos = 0;
+
+// $code = ord($this->data[$pos]) | ord($this->data[$pos+1])<<8;
+ $length = ord( $this->data[ $pos + 2 ] ) | ord( $this->data[ $pos + 3 ] ) << 8;
+
+ $version = ord( $this->data[ $pos + 4 ] ) | ord( $this->data[ $pos + 5 ] ) << 8;
+ $substreamType = ord( $this->data[ $pos + 6 ] ) | ord( $this->data[ $pos + 7 ] ) << 8;
+// echo "Start parse code=".base_convert($code,10,16)." version=".base_convert($version,10,16)." substreamType=".base_convert($substreamType,10,16).""."\n";
+
+// die();
+
+ if ( ( $version !== self::BIFF8 ) &&
+ ( $version !== self::BIFF7 )
+ ) {
+ return false;
+ }
+
+ if ( $substreamType !== self::WORKBOOKGLOBALS ) {
+ return false;
+ }
+
+ //print_r($rec);
+ $pos += $length + 4;
+
+ $code = ord( $this->data[ $pos ] ) | ord( $this->data[ $pos + 1 ] ) << 8;
+ $length = ord( $this->data[ $pos + 2 ] ) | ord( $this->data[ $pos + 3 ] ) << 8;
+
+ while ( $code !== self::TYPE_EOF ) {
+ switch ( $code ) {
+ case self::TYPE_SST:
+ //echo "Type_SST\n";
+ $formattingRuns = 0;
+ $extendedRunLength = 0;
+ $spos = $pos + 4;
+ $limitpos = $spos + $length;
+ $uniqueStrings = $this->_GetInt4d( $this->data, $spos + 4 );
+ $spos += 8;
+ for ( $i = 0; $i < $uniqueStrings; $i ++ ) {
+ // Read in the number of characters
+ if ( $spos === $limitpos ) {
+ $opcode = ord( $this->data[ $spos ] ) | ord( $this->data[ $spos + 1 ] ) << 8;
+ $conlength = ord( $this->data[ $spos + 2 ] ) | ord( $this->data[ $spos + 3 ] ) << 8;
+ if ( $opcode !== 0x3c ) {
+ return - 1;
+ }
+ $spos += 4;
+ $limitpos = $spos + $conlength;
+ }
+ $numChars = ord( $this->data[ $spos ] ) | ( ord( $this->data[ $spos + 1 ] ) << 8 );
+ //echo "i = $i pos = $pos numChars = $numChars ";
+ $spos += 2;
+ $optionFlags = ord( $this->data[ $spos ] );
+ $spos ++;
+ $asciiEncoding = ( ( $optionFlags & 0x01 ) === 0 );
+ $extendedString = ( ( $optionFlags & 0x04 ) !== 0 );
+
+ // See if string contains formatting information
+ $richString = ( ( $optionFlags & 0x08 ) !== 0 );
+
+ if ( $richString ) {
+ // Read in the crun
+ $formattingRuns = ord( $this->data[ $spos ] ) | ( ord( $this->data[ $spos + 1 ] ) << 8 );
+ $spos += 2;
+ }
+
+ if ( $extendedString ) {
+ // Read in cchExtRst
+ $extendedRunLength = $this->_GetInt4d( $this->data, $spos );
+ $spos += 4;
+ }
+
+ $len = $asciiEncoding ? $numChars : $numChars * 2;
+ if ( $spos + $len < $limitpos ) {
+ $retstr = $this->_substr( $this->data, $spos, $len );
+ $spos += $len;
+ } else {
+ // found countinue
+ $retstr = $this->_substr( $this->data, $spos, $limitpos - $spos );
+ $bytesRead = $limitpos - $spos;
+ $charsLeft = $numChars - ( $asciiEncoding ? $bytesRead : ( $bytesRead / 2 ) );
+ $spos = $limitpos;
+
+ while ( $charsLeft > 0 ) {
+ $opcode = ord( $this->data[ $spos ] ) | ord( $this->data[ $spos + 1 ] ) << 8;
+ $conlength = ord( $this->data[ $spos + 2 ] ) | ord( $this->data[ $spos + 3 ] ) << 8;
+ if ( $opcode !== 0x3c ) {
+ return - 1;
+ }
+ $spos += 4;
+ $limitpos = $spos + $conlength;
+ $option = ord( $this->data[ $spos ] );
+ $spos ++;
+ if ( $asciiEncoding && ( $option === 0 ) ) {
+ $len = min( $charsLeft, $limitpos - $spos ); // min($charsLeft, $conlength);
+ $retstr .= $this->_substr( $this->data, $spos, $len );
+ $charsLeft -= $len;
+ $asciiEncoding = true;
+ } elseif ( ! $asciiEncoding && ( $option !== 0 ) ) {
+ $len = min( $charsLeft * 2, $limitpos - $spos ); // min($charsLeft, $conlength);
+ $retstr .= $this->_substr( $this->data, $spos, $len );
+ $charsLeft -= $len / 2;
+ $asciiEncoding = false;
+ } elseif ( ! $asciiEncoding && ( $option === 0 ) ) {
+ // Bummer - the string starts off as Unicode, but after the
+ // continuation it is in straightforward ASCII encoding
+ $len = min( $charsLeft, $limitpos - $spos ); // min($charsLeft, $conlength);
+ for ( $j = 0; $j < $len; $j ++ ) {
+ $retstr .= $this->data[ $spos + $j ] . chr( 0 );
+ }
+ $charsLeft -= $len;
+ $asciiEncoding = false;
+ } else {
+ $newstr = '';
+ for ( $j = 0, $len_retstr = $this->_strlen( $retstr ); $j < $len_retstr; $j ++ ) {
+ $newstr = $retstr[ $j ] . chr( 0 );
+ }
+ $retstr = $newstr;
+ $len = min( $charsLeft * 2, $limitpos - $spos ); // min($charsLeft, $conlength);
+ $retstr .= $this->_substr( $this->data, $spos, $len );
+ $charsLeft -= $len / 2;
+ $asciiEncoding = false;
+ //echo "Izavrat\n";
+ }
+ $spos += $len;
+
+ }
+ }
+ $retstr = $asciiEncoding ? $retstr : $this->_encodeUTF16( $retstr );
+// echo "Str $i = $retstr\n";
+ if ( $richString ) {
+ $spos += 4 * $formattingRuns;
+ }
+
+ // For extended strings, skip over the extended string data
+ if ( $extendedString ) {
+ $spos += $extendedRunLength;
+ }
+ //if ($retstr == 'Derby'){
+ // echo "bb\n";
+ //}
+ $this->sst[] = $retstr;
+ }
+ /*$continueRecords = array();
+ while ($this->getNextCode() == Type_CONTINUE) {
+ $continueRecords[] = &$this->nextRecord();
+ }
+ //echo " 1 Type_SST\n";
+ $this->shareStrings = new SSTRecord($r, $continueRecords);
+ //print_r($this->shareStrings->strings);
+ */
+ // echo 'SST read: '.($time_end-$time_start)."\n";
+ break;
+
+ case self::TYPE_FILEPASS:
+ return false;
+ break;
+ case self::TYPE_NAME:
+ //echo "Type_NAME\n";
+ break;
+ case self::TYPE_FORMAT:
+ $indexCode = ord( $this->data[ $pos + 4 ] ) | ord( $this->data[ $pos + 5 ] ) << 8;
+
+ if ( $version === self::BIFF8 ) {
+ $numchars = ord( $this->data[ $pos + 6 ] ) | ord( $this->data[ $pos + 7 ] ) << 8;
+ if ( ord( $this->data[ $pos + 8 ] ) === 0 ) {
+ $formatString = $this->_substr( $this->data, $pos + 9, $numchars );
+ } else {
+ $formatString = $this->_substr( $this->data, $pos + 9, $numchars * 2 );
+ }
+ } else {
+ $numchars = ord( $this->data[ $pos + 6 ] );
+ $formatString = $this->_substr( $this->data, $pos + 7, $numchars * 2 );
+ }
+
+ $this->formatRecords[ $indexCode ] = $formatString;
+ // echo "Type.FORMAT\n";
+ break;
+ case self::TYPE_XF:
+ $formatstr = '';
+ //global $dateFormats, $numberFormats;
+ $indexCode = ord( $this->data[ $pos + 6 ] ) | ord( $this->data[ $pos + 7 ] ) << 8;
+ //echo "\nType.XF ".count($this->formatRecords['xfrecords'])." $indexCode ";
+ if ( array_key_exists( $indexCode, $this->dateFormats ) ) {
+ //echo "isdate ".$dateFormats[$indexCode];
+ $this->formatRecords['xfrecords'][] = array(
+ 'type' => 'date',
+ 'format' => $this->dateFormats[ $indexCode ]
+ );
+ } elseif ( array_key_exists( $indexCode, $this->numberFormats ) ) {
+ //echo "isnumber ".$this->numberFormats[$indexCode];
+ $this->formatRecords['xfrecords'][] = array(
+ 'type' => 'number',
+ 'format' => $this->numberFormats[ $indexCode ]
+ );
+ } else {
+ $isdate = false;
+ if ( $indexCode > 0 ) {
+ if ( isset( $this->formatRecords[ $indexCode ] ) ) {
+ $formatstr = $this->formatRecords[ $indexCode ];
+ }
+ //echo '.other.';
+// echo "\ndate-time=$formatstr=\n";
+ if ( $formatstr && preg_match( "/^[hmsday\/\-:\s]+$/i", $formatstr ) === 1 ) { // found day and time format
+ $isdate = true;
+ $formatstr = str_replace( array( 'mm', 'h' ), array( 'i', 'H' ), $formatstr );
+ //echo "\ndate-time $formatstr \n";
+ }
+ }
+
+ if ( $isdate ) {
+ $this->formatRecords['xfrecords'][] = array(
+ 'type' => 'date',
+ 'format' => $formatstr,
+ );
+ } else {
+ $this->formatRecords['xfrecords'][] = array(
+ 'type' => 'other',
+ 'format' => '',
+ 'code' => $indexCode
+ );
+ }
+ }
+ //echo "\n";
+ break;
+ case self::TYPE_NINETEENFOUR:
+ //echo "Type.NINETEENFOUR\n";
+ $this->nineteenFour = ( ord( $this->data[ $pos + 4 ] ) === 1 );
+ break;
+ case self::TYPE_BOUNDSHEET:
+ //echo "Type.BOUNDSHEET\n";
+ $rec_offset = $this->_GetInt4d( $this->data, $pos + 4 );
+// $rec_typeFlag = ord($this->_data[$pos + 8]);
+// $rec_visibilityFlag = ord($this->_data[$pos + 9]);
+ $rec_length = ord( $this->data[ $pos + 10 ] );
+ $rec_name = '';
+ if ( $version === self::BIFF8 ) {
+ $chartype = ord( $this->data[ $pos + 11 ] );
+ if ( $chartype === 0 ) {
+ $rec_name = $this->_substr( $this->data, $pos + 12, $rec_length );
+ } else {
+ $rec_name = $this->_encodeUTF16( $this->_substr( $this->data, $pos + 12, $rec_length * 2 ) );
+ }
+ } elseif ( $version === self::BIFF7 ) {
+ $rec_name = $this->_substr( $this->data, $pos + 11, $rec_length );
+ }
+ $this->boundsheets[] = array(
+ 'name' => $rec_name,
+ 'offset' => $rec_offset
+ );
+
+ break;
+
+ }
+
+ //echo "Code = ".base_convert($r['code'],10,16)."\n";
+ $pos += $length + 4;
+ $code = ord( $this->data[ $pos ] ) | ord( $this->data[ $pos + 1 ] ) << 8;
+ $length = ord( $this->data[ $pos + 2 ] ) | ord( $this->data[ $pos + 3 ] ) << 8;
+
+ //$r = &$this->nextRecord();
+ //echo "1 Code = ".base_convert($r['code'],10,16)."\n";
+ }
+
+ foreach ( $this->boundsheets as $key => $val ) {
+ $this->sn = $key;
+ $this->_parsesheet( $val['offset'] );
+ }
+
+ return true;
+
+ }
+
+ public function _encodeUTF16( $string ) {
+ $result = $string;
+ if ( $this->defaultEncoding ) {
+ $result = mb_convert_encoding( $string, $this->defaultEncoding, 'UTF-16LE' );
+ }
+
+ return $result;
+ }
+
+ public function _parsesheet( $spos ) {
+ $cont = true;
+ // read BOF
+// $code = ord($this->_data[$spos]) | ord($this->_data[$spos + 1]) << 8;
+ $length = ord( $this->data[ $spos + 2 ] ) | ord( $this->data[ $spos + 3 ] ) << 8;
+
+ $version = ord( $this->data[ $spos + 4 ] ) | ord( $this->data[ $spos + 5 ] ) << 8;
+ $substreamType = ord( $this->data[ $spos + 6 ] ) | ord( $this->data[ $spos + 7 ] ) << 8;
+
+ if ( ( $version !== self::BIFF8 ) && ( $version !== self::BIFF7 ) ) {
+ return - 1;
+ }
+
+ if ( $substreamType !== self::WORKSHEET ) {
+ return - 2;
+ }
+ //echo "Start parse code=".base_convert($code,10,16)." version=".base_convert($version,10,16)." substreamType=".base_convert($substreamType,10,16).""."\n";
+ $spos += $length + 4;
+ //var_dump($this->formatRecords);
+ //echo "code $code $length";
+ while ( $cont ) {
+ //echo "mem= ".memory_get_usage()."\n";
+// $r = &$this->file->nextRecord();
+ $lowcode = ord( $this->data[ $spos ] );
+ if ( $lowcode === self::TYPE_EOF ) {
+ break;
+ }
+ $code = $lowcode | ord( $this->data[ $spos + 1 ] ) << 8;
+ $length = ord( $this->data[ $spos + 2 ] ) | ord( $this->data[ $spos + 3 ] ) << 8;
+ $spos += 4;
+ $this->sheets[ $this->sn ]['maxrow'] = $this->rowoffset - 1;
+ $this->sheets[ $this->sn ]['maxcol'] = $this->colOffset - 1;
+ //echo "Code=".base_convert($code,10,16)." $code\n";
+ unset( $this->recType );
+ $this->multiplier = 1; // need for format with %
+ switch ( $code ) {
+ case self::TYPE_DIMENSION:
+ //echo 'Type_DIMENSION ';
+ if ( ! isset( $this->numRows ) ) {
+ if ( ( $length === 10 ) || ( $version === self::BIFF7 ) ) {
+ $this->sheets[ $this->sn ]['numRows'] = ord( $this->data[ $spos + 2 ] ) | ord( $this->data[ $spos + 3 ] ) << 8;
+ $this->sheets[ $this->sn ]['numCols'] = ord( $this->data[ $spos + 6 ] ) | ord( $this->data[ $spos + 7 ] ) << 8;
+ } else {
+ $this->sheets[ $this->sn ]['numRows'] = ord( $this->data[ $spos + 4 ] ) | ord( $this->data[ $spos + 5 ] ) << 8;
+ $this->sheets[ $this->sn ]['numCols'] = ord( $this->data[ $spos + 10 ] ) | ord( $this->data[ $spos + 11 ] ) << 8;
+ }
+ }
+ //echo 'numRows '.$this->numRows.' '.$this->numCols."\n";
+ break;
+ case self::TYPE_MERGEDCELLS:
+ $cellRanges = ord( $this->data[ $spos ] ) | ord( $this->data[ $spos + 1 ] ) << 8;
+ for ( $i = 0; $i < $cellRanges; $i ++ ) {
+ $fr = ord( $this->data[ $spos + 8 * $i + 2 ] ) | ord( $this->data[ $spos + 8 * $i + 3 ] ) << 8;
+ $lr = ord( $this->data[ $spos + 8 * $i + 4 ] ) | ord( $this->data[ $spos + 8 * $i + 5 ] ) << 8;
+ $fc = ord( $this->data[ $spos + 8 * $i + 6 ] ) | ord( $this->data[ $spos + 8 * $i + 7 ] ) << 8;
+ $lc = ord( $this->data[ $spos + 8 * $i + 8 ] ) | ord( $this->data[ $spos + 8 * $i + 9 ] ) << 8;
+ //$this->sheets[$this->sn]['mergedCells'][] = array($fr + 1, $fc + 1, $lr + 1, $lc + 1);
+ if ( $lr - $fr > 0 ) {
+ $this->sheets[ $this->sn ]['cellsInfo'][ $fr + 1 ][ $fc + 1 ]['rowspan'] = $lr - $fr + 1;
+ }
+ if ( $lc - $fc > 0 ) {
+ $this->sheets[ $this->sn ]['cellsInfo'][ $fr + 1 ][ $fc + 1 ]['colspan'] = $lc - $fc + 1;
+ }
+ }
+ //echo "Merged Cells $cellRanges $lr $fr $lc $fc\n";
+ break;
+ case self::TYPE_RK:
+ case self::TYPE_RK2:
+ //echo 'self::TYPE_RK'."\n";
+ $row = ord( $this->data[ $spos ] ) | ord( $this->data[ $spos + 1 ] ) << 8;
+ $column = ord( $this->data[ $spos + 2 ] ) | ord( $this->data[ $spos + 3 ] ) << 8;
+ $rknum = $this->_GetInt4d( $this->data, $spos + 6 );
+ $numValue = $this->_GetIEEE754( $rknum );
+ //echo $numValue." ";
+ if ( $this->isDate( $spos ) ) {
+ list( $string, $raw ) = $this->createDate( $numValue );
+ } else {
+ $raw = $numValue;
+ if ( isset( $this->columnsFormat[ $column + 1 ] ) ) {
+ $this->curFormat = $this->columnsFormat[ $column + 1 ];
+ }
+ $string = sprintf( $this->curFormat, $numValue * $this->multiplier );
+ //$this->addcell(RKRecord($r));
+ }
+ $this->addcell( $row, $column, $string, $raw );
+ //echo "Type_RK $row $column $string $raw {$this->curformat}\n";
+ break;
+ case self::TYPE_LABELSST:
+ $row = ord( $this->data[ $spos ] ) | ord( $this->data[ $spos + 1 ] ) << 8;
+ $column = ord( $this->data[ $spos + 2 ] ) | ord( $this->data[ $spos + 3 ] ) << 8;
+// $xfindex = ord($this->_data[$spos + 4]) | ord($this->_data[$spos + 5]) << 8;
+ $index = $this->_GetInt4d( $this->data, $spos + 6 );
+ //var_dump($this->sst);
+ $this->addcell( $row, $column, $this->sst[ $index ] );
+ //echo "LabelSST $row $column $string\n";
+ break;
+ case self::TYPE_MULRK:
+ $row = ord( $this->data[ $spos ] ) | ord( $this->data[ $spos + 1 ] ) << 8;
+ $colFirst = ord( $this->data[ $spos + 2 ] ) | ord( $this->data[ $spos + 3 ] ) << 8;
+ $colLast = ord( $this->data[ $spos + $length - 2 ] ) | ord( $this->data[ $spos + $length - 1 ] ) << 8;
+ $columns = $colLast - $colFirst + 1;
+ $tmppos = $spos + 4;
+ for ( $i = 0; $i < $columns; $i ++ ) {
+ $numValue = $this->_GetIEEE754( $this->_GetInt4d( $this->data, $tmppos + 2 ) );
+ if ( $this->isDate( $tmppos - 4 ) ) {
+ list( $string, $raw ) = $this->createDate( $numValue );
+ } else {
+ $raw = $numValue;
+ if ( isset( $this->columnsFormat[ $colFirst + $i + 1 ] ) ) {
+ $this->curFormat = $this->columnsFormat[ $colFirst + $i + 1 ];
+ }
+ $string = sprintf( $this->curFormat, $numValue * $this->multiplier );
+ }
+ //$rec['rknumbers'][$i]['xfindex'] = ord($rec['data'][$pos]) | ord($rec['data'][$pos+1]) << 8;
+ $tmppos += 6;
+ $this->addcell( $row, $colFirst + $i, $string, $raw );
+ //echo "MULRK $row ".($colFirst + $i)." $string\n";
+ }
+ //MulRKRecord($r);
+ // Get the individual cell records from the multiple record
+ //$num = ;
+
+ break;
+ case self::TYPE_NUMBER:
+ $row = ord( $this->data[ $spos ] ) | ord( $this->data[ $spos + 1 ] ) << 8;
+ $column = ord( $this->data[ $spos + 2 ] ) | ord( $this->data[ $spos + 3 ] ) << 8;
+ $tmp = unpack( 'ddouble', $this->_substr( $this->data, $spos + 6, 8 ) ); // It machine machine dependent
+ if ( $this->isDate( $spos ) ) {
+ list( $string, $raw ) = $this->createDate( $tmp['double'] );
+ // $this->addcell(DateRecord($r, 1));
+ } else {
+ //$raw = $tmp[''];
+ if ( isset( $this->columnsFormat[ $column + 1 ] ) ) {
+ $this->curFormat = $this->columnsFormat[ $column + 1 ];
+ }
+ $raw = $this->createNumber( $spos );
+ $string = sprintf( $this->curFormat, $raw * $this->multiplier );
+
+ // $this->addcell(NumberRecord($r));
+ }
+ $this->addcell( $row, $column, $string, $raw );
+ //echo "Number $row $column $string\n";
+ break;
+ case self::TYPE_FORMULA:
+ case self::TYPE_FORMULA2:
+ $row = ord( $this->data[ $spos ] ) | ord( $this->data[ $spos + 1 ] ) << 8;
+ $column = ord( $this->data[ $spos + 2 ] ) | ord( $this->data[ $spos + 3 ] ) << 8;
+ /*
+ $byte6 = ord($this->_data[$spos + 6]);
+ $byte12 = ord($this->_data[$spos + 12]);
+ $byte13 = ord($this->_data[$spos + 13]);
+
+ if ( $byte6 === 0 && $byte12 === 255 && $byte13 === 255 ) {
+ //String formula. Result follows in a STRING record
+ //echo "FORMULA $row $column Formula with a string \n";
+ } else if ($byte6 === 1 && $byte12 === 255 && $byte13 === 255 ) {
+ //Boolean formula. Result is in +2; 0=false,1=true
+ } else if ($byte6 === 2 && $byte12 === 255 && $byte13 === 255) {
+ //Error formula. Error code is in +2;
+ } else if ( $byte6 === 3 && $byte12 === 255 && $byte13 === 255) {
+ //Formula result is a null string.
+ */
+ if ( ! ( ord( $this->data[ $spos + 6 ] ) < 4 && ord( $this->data[ $spos + 12 ] ) === 255 && ord( $this->data[ $spos + 13 ] ) === 255 ) ) {
+ // result is a number, so first 14 bytes are just like a _NUMBER record
+ $tmp = unpack( 'ddouble', $this->_substr( $this->data, $spos + 6, 8 ) ); // It machine machine dependent
+ if ( $this->isDate( $spos ) ) {
+ list( $string, $raw ) = $this->createDate( $tmp['double'] );
+ // $this->addcell(DateRecord($r, 1));
+ } else {
+ //$raw = $tmp[''];
+ if ( isset( $this->columnsFormat[ $column + 1 ] ) ) {
+ $this->curFormat = $this->columnsFormat[ $column + 1 ];
+ }
+ $raw = $this->createNumber( $spos );
+ $string = sprintf( $this->curFormat, $raw * $this->multiplier );
+
+ // $this->addcell(NumberRecord($r));
+ }
+ $this->addcell( $row, $column, $string, $raw );
+ //echo "Number $row $column $string\n";
+ }
+ break;
+ case self::TYPE_BOOLERR:
+ $row = ord( $this->data[ $spos ] ) | ord( $this->data[ $spos + 1 ] ) << 8;
+ $column = ord( $this->data[ $spos + 2 ] ) | ord( $this->data[ $spos + 3 ] ) << 8;
+ $string = ord( $this->data[ $spos + 6 ] );
+ $this->addcell( $row, $column, $string );
+ //echo 'Type_BOOLERR '."\n";
+ break;
+ case self::TYPE_ROW:
+ case self::TYPE_DBCELL:
+ case self::TYPE_MULBLANK:
+ break;
+ case self::TYPE_LABEL:
+ $row = ord( $this->data[ $spos ] ) | ord( $this->data[ $spos + 1 ] ) << 8;
+ $column = ord( $this->data[ $spos + 2 ] ) | ord( $this->data[ $spos + 3 ] ) << 8;
+ $this->addcell( $row, $column, $this->_substr( $this->data, $spos + 8, ord( $this->data[ $spos + 6 ] ) | ord( $this->data[ $spos + 7 ] ) << 8 ) );
+
+ // $this->addcell(LabelRecord($r));
+ break;
+
+ case self::TYPE_EOF:
+ $cont = false;
+ break;
+ default:
+ //echo ' unknown :'.base_convert($r['code'],10,16)."\n";
+ break;
+
+ }
+ $spos += $length;
+ }
+
+ if ( ! isset( $this->sheets[ $this->sn ]['numRows'] ) ) {
+ $this->sheets[ $this->sn ]['numRows'] = $this->sheets[ $this->sn ]['maxrow'];
+ }
+ if ( ! isset( $this->sheets[ $this->sn ]['numCols'] ) ) {
+ $this->sheets[ $this->sn ]['numCols'] = $this->sheets[ $this->sn ]['maxcol'];
+ }
+
+ return true;
+ }
+
+ //}}}
+ //{{{ createDate()
+
+ public function _GetIEEE754( $rknum ) {
+ if ( ( $rknum & 0x02 ) !== 0 ) {
+ $value = $rknum >> 2;
+ } else {
+//mmp
+// first comment out the previously existing 7 lines of code here
+// $tmp = unpack("d", pack("VV", 0, ($rknum & 0xfffffffc)));
+// //$value = $tmp[''];
+// if (array_key_exists(1, $tmp)) {
+// $value = $tmp[1];
+// } else {
+// $value = $tmp[''];
+// }
+// I got my info on IEEE754 encoding from
+// http://research.microsoft.com/~hollasch/cgindex/coding/ieeefloat.html
+// The RK format calls for using only the most significant 30 bits of the
+// 64 bit floating point value. The other 34 bits are assumed to be 0
+// So, we use the upper 30 bits of $rknum as follows...
+ $sign = ( $rknum & 0x80000000 ) >> 31;
+ $exp = ( $rknum & 0x7ff00000 ) >> 20;
+ $mantissa = ( 0x100000 | ( $rknum & 0x000ffffc ) );
+ $value = $mantissa / pow( 2, 20 - ( $exp - 1023 ) );
+ if ( $sign ) {
+ $value = - 1 * $value;
+ }
+//end of changes by mmp
+
+ }
+
+ if ( ( $rknum & 0x01 ) !== 0 ) {
+ $value /= 100;
+ }
+
+ return $value;
+ }
+
+ public function isDate( $spos ) {
+ //$xfindex = GetInt2d(, 4);
+ $xfindex = ord( $this->data[ $spos + 4 ] ) | ord( $this->data[ $spos + 5 ] ) << 8;
+ //echo 'check is date '.$xfindex.' '.$this->formatRecords['xfrecords'][$xfindex]['type']."\n";
+ //var_dump($this->formatRecords['xfrecords'][$xfindex]);
+ if ( $this->formatRecords['xfrecords'][ $xfindex ]['type'] === 'date' ) {
+ $this->curFormat = $this->formatRecords['xfrecords'][ $xfindex ]['format'];
+ $this->recType = 'date';
+
+ return true;
+ }
+
+ if ( $this->formatRecords['xfrecords'][ $xfindex ]['type'] === 'number' ) {
+ $this->curFormat = $this->formatRecords['xfrecords'][ $xfindex ]['format'];
+ $this->recType = 'number';
+ if ( ( $xfindex === 0x9 ) || ( $xfindex === 0xa ) ) {
+ $this->multiplier = 100;
+ }
+ } else {
+ $this->curFormat = $this->defaultFormat;
+ $this->recType = 'unknown';
+ }
+
+ return false;
+ }
+
+ /**
+ * Convert the raw Excel date into a human readable format
+ *
+ * Dates in Excel are stored as number of seconds from an epoch. On
+ * Windows, the epoch is 30/12/1899 and on Mac it's 01/01/1904
+ *
+ * @param integer $timevalue The raw Excel value to convert
+ *
+ * @return array First element is the converted date, the second element is number a unix timestamp
+ */
+ public function createDate( $timevalue ) {
+// $offset = ($timeoffset===null)? date('Z') : $timeoffset * 3600;
+ if ($timevalue > 1) {
+ $timevalue -= ( $this->nineteenFour ? 24107 : 25569 );
+ }
+ $ts = round($timevalue * 24 * 3600);
+ $string = $this->datetimeFormat ? gmdate( $this->datetimeFormat, $ts ) : gmdate( $this->curFormat, $ts );
+ return array( $string, $ts );
+
+ }
+
+ public function addcell( $row, $col, $string, $raw = '' ) {
+ //echo "ADD cel $row-$col $string\n";
+ $this->sheets[ $this->sn ]['maxrow'] = max( $this->sheets[ $this->sn ]['maxrow'], $row + $this->rowoffset );
+ $this->sheets[ $this->sn ]['maxcol'] = max( $this->sheets[ $this->sn ]['maxcol'], $col + $this->colOffset );
+ $this->sheets[ $this->sn ]['cells'][ $row + $this->rowoffset ][ $col + $this->colOffset ] = $string;
+ if ( $raw ) {
+ $this->sheets[ $this->sn ]['cellsInfo'][ $row + $this->rowoffset ][ $col + $this->colOffset ]['raw'] = $raw;
+ }
+ if ( isset( $this->recType ) ) {
+ $this->sheets[ $this->sn ]['cellsInfo'][ $row + $this->rowoffset ][ $col + $this->colOffset ]['type'] = $this->recType;
+ }
+
+ }
+
+ public function createNumber( $spos ) {
+ $rknumhigh = $this->_GetInt4d( $this->data, $spos + 10 );
+ $rknumlow = $this->_GetInt4d( $this->data, $spos + 6 );
+ //for ($i=0; $i<8; $i++) { echo ord($this->_data[$i+$spos+6]) . " "; } echo " ";
+ $sign = ( $rknumhigh & 0x80000000 ) >> 31;
+ $exp = ( $rknumhigh & 0x7ff00000 ) >> 20;
+ $mantissa = ( 0x100000 | ( $rknumhigh & 0x000fffff ) );
+ $mantissalow1 = ( $rknumlow & 0x80000000 ) >> 31;
+ $mantissalow2 = ( $rknumlow & 0x7fffffff );
+ $value = $mantissa / pow( 2, 20 - ( $exp - 1023 ) );
+ if ( $mantissalow1 !== 0 ) {
+ $value += 1 / pow( 2, 21 - ( $exp - 1023 ) );
+ }
+ $value += $mantissalow2 / pow( 2, 52 - ( $exp - 1023 ) );
+ //echo "Sign = $sign, Exp = $exp, mantissahighx = $mantissa, mantissalow1 = $mantissalow1, mantissalow2 = $mantissalow2 \n";
+ if ( $sign ) {
+ $value = - 1 * $value;
+ }
+
+ return $value;
+ }
+
+ /**
+ * Set the encoding method
+ *
+ * @param string $encoding Encoding to use
+ *
+ * @access public
+ */
+ public function setOutputEncoding( $encoding ) {
+ $this->defaultEncoding = $encoding;
+ }
+
+ public function setRowColOffset( $iOffset ) {
+ $this->rowoffset = $iOffset;
+ $this->colOffset = $iOffset;
+ }
+
+ /**
+ * Set the default number format
+ *
+ * @access public
+ *
+ * @param string $sFormat Default format
+ */
+ public function setDefaultFormat( $sFormat ) {
+ $this->defaultFormat = $sFormat;
+ }
+
+ /**
+ * Force a column to use a certain format
+ *
+ * @access public
+ *
+ * @param integer $column Column number
+ * @param string $sFormat Format
+ */
+ public function setColumnFormat( $column, $sFormat ) {
+ $this->columnsFormat[ $column ] = $sFormat;
+ }
+ private function _strlen( $str ) {
+ return (ini_get('mbstring.func_overload') & 2) ? mb_strlen($str , '8bit') : strlen($str);
+ }
+ private function _strpos( $haystack, $needle, $offset = 0 ) {
+ return (ini_get('mbstring.func_overload') & 2) ? mb_strpos( $haystack, $needle, $offset , '8bit') : strpos($haystack, $needle, $offset);
+ }
+ private function _substr( $str, $start, $length = null ) {
+ return (ini_get('mbstring.func_overload') & 2) ? mb_substr( $str, $start, ($length === null) ? mb_strlen($str,'8bit') : $length, '8bit') : substr($str, $start, ($length === null) ? strlen($str) : $length );
+ }
+}
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
\ No newline at end of file
diff --git a/modules/import_api/lib/SimpleXLSX.php b/modules/import_api/lib/SimpleXLSX.php
new file mode 100644
index 00000000..661e39e1
--- /dev/null
+++ b/modules/import_api/lib/SimpleXLSX.php
@@ -0,0 +1,1056 @@
+rows() );
+ * } else {
+ * echo SimpleXLSX::parseError();
+ * }
+ *
+ * Example 2: html table
+ * if ( $xlsx = SimpleXLSX::parse('book.xlsx') ) {
+ * echo $xlsx->toHTML();
+ * } else {
+ * echo SimpleXLSX::parseError();
+ * }
+ *
+ * Example 3: rowsEx
+ * $xlsx = SimpleXLSX::parse('book.xlsx');
+ * print_r( $xlsx->rowsEx() );
+ *
+ * Example 4: select worksheet
+ * $xlsx = SimpleXLSX::parse('book.xlsx');
+ * print_r( $xlsx->rows(1) ); // second worksheet
+ *
+ * Example 5: IDs and worksheet names
+ * $xlsx = SimpleXLSX::parse('book.xlsx');
+ * print_r( $xlsx->sheetNames() ); // array( 0 => 'Sheet 1', 1 => 'Catalog' );
+ *
+ * Example 6: get sheet name by index
+ * $xlsx = SimpleXLSX::parse('book.xlsx');
+ * echo 'Sheet Name 2 = '.$xlsx->sheetName(1);
+ *
+ * Example 7: getCell (very slow)
+ * echo $xlsx->getCell(1,'D12'); // reads D12 cell from second sheet
+ *
+ * Example 8: read data
+ * if ( $xlsx = SimpleXLSX::parseData( file_get_contents('http://www.example.com/example.xlsx') ) ) {
+ * $dim = $xlsx->dimension(1);
+ * $num_cols = $dim[0];
+ * $num_rows = $dim[1];
+ * echo $xlsx->sheetName(1).':'.$num_cols.'x'.$num_rows;
+ * } else {
+ * echo SimpleXLSX::parseError();
+ * }
+ *
+ * Example 9: old style
+ * $xlsx = new SimpleXLSX('book.xlsx');
+ * if ( $xlsx->success() ) {
+ * print_r( $xlsx->rows() );
+ * } else {
+ * echo 'xlsx error: '.$xlsx->error();
+ * }
+ */
+
+/** @noinspection PhpUndefinedFieldInspection */
+/** @noinspection PhpComposerExtensionStubsInspection */
+/** @noinspection MultiAssignmentUsageInspection */
+
+class SimpleXLSX {
+ // Don't remove this string! Created by Sergey Shuchkin sergey.shuchkin@gmail.com
+ public static $CF = [ // Cell formats
+ 0 => 'General',
+ 1 => '0',
+ 2 => '0.00',
+ 3 => '#,##0',
+ 4 => '#,##0.00',
+ 9 => '0%',
+ 10 => '0.00%',
+ 11 => '0.00E+00',
+ 12 => '# ?/?',
+ 13 => '# ??/??',
+ 14 => 'mm-dd-yy',
+ 15 => 'd-mmm-yy',
+ 16 => 'd-mmm',
+ 17 => 'mmm-yy',
+ 18 => 'h:mm AM/PM',
+ 19 => 'h:mm:ss AM/PM',
+ 20 => 'h:mm',
+ 21 => 'h:mm:ss',
+ 22 => 'm/d/yy h:mm',
+
+ 37 => '#,##0 ;(#,##0)',
+ 38 => '#,##0 ;[Red](#,##0)',
+ 39 => '#,##0.00;(#,##0.00)',
+ 40 => '#,##0.00;[Red](#,##0.00)',
+
+ 44 => '_("$"* #,##0.00_);_("$"* \(#,##0.00\);_("$"* "-"??_);_(@_)',
+ 45 => 'mm:ss',
+ 46 => '[h]:mm:ss',
+ 47 => 'mmss.0',
+ 48 => '##0.0E+0',
+ 49 => '@',
+
+ 27 => '[$-404]e/m/d',
+ 30 => 'm/d/yy',
+ 36 => '[$-404]e/m/d',
+ 50 => '[$-404]e/m/d',
+ 57 => '[$-404]e/m/d',
+
+ 59 => 't0',
+ 60 => 't0.00',
+ 61 => 't#,##0',
+ 62 => 't#,##0.00',
+ 67 => 't0%',
+ 68 => 't0.00%',
+ 69 => 't# ?/?',
+ 70 => 't# ??/??',
+ ];
+ public $cellFormats = [];
+ public $datetimeFormat = 'Y-m-d H:i:s';
+ public $debug;
+
+ /* @var SimpleXMLElement[] $sheets */
+ private $sheets;
+ private $sheetNames = [];
+ // scheme
+ private $styles;
+ private $hyperlinks;
+ /* @var array[] $package */
+ private $package;
+ private $sharedstrings;
+ private $date1904 = 0;
+
+
+ /*
+ private $date_formats = array(
+ 0xe => "d/m/Y",
+ 0xf => "d-M-Y",
+ 0x10 => "d-M",
+ 0x11 => "M-Y",
+ 0x12 => "h:i a",
+ 0x13 => "h:i:s a",
+ 0x14 => "H:i",
+ 0x15 => "H:i:s",
+ 0x16 => "d/m/Y H:i",
+ 0x2d => "i:s",
+ 0x2e => "H:i:s",
+ 0x2f => "i:s.S"
+ );
+ private $number_formats = array(
+ 0x1 => "%1.0f", // "0"
+ 0x2 => "%1.2f", // "0.00",
+ 0x3 => "%1.0f", //"#,##0",
+ 0x4 => "%1.2f", //"#,##0.00",
+ 0x5 => "%1.0f", //"$#,##0;($#,##0)",
+ 0x6 => '$%1.0f', //"$#,##0;($#,##0)",
+ 0x7 => '$%1.2f', //"$#,##0.00;($#,##0.00)",
+ 0x8 => '$%1.2f', //"$#,##0.00;($#,##0.00)",
+ 0x9 => '%1.0f%%', //"0%"
+ 0xa => '%1.2f%%', //"0.00%"
+ 0xb => '%1.2f', //"0.00E00",
+ 0x25 => '%1.0f', //"#,##0;(#,##0)",
+ 0x26 => '%1.0f', //"#,##0;(#,##0)",
+ 0x27 => '%1.2f', //"#,##0.00;(#,##0.00)",
+ 0x28 => '%1.2f', //"#,##0.00;(#,##0.00)",
+ 0x29 => '%1.0f', //"#,##0;(#,##0)",
+ 0x2a => '$%1.0f', //"$#,##0;($#,##0)",
+ 0x2b => '%1.2f', //"#,##0.00;(#,##0.00)",
+ 0x2c => '$%1.2f', //"$#,##0.00;($#,##0.00)",
+ 0x30 => '%1.0f'); //"##0.0E0";
+ // }}}
+ */
+ private $errno = 0;
+ private $error = false;
+
+
+ public function __construct( $filename = null, $is_data = null, $debug = null ) {
+ if ( $debug !== null ) {
+ $this->debug = $debug;
+ }
+ $this->package = [
+ 'filename' => '',
+ 'mtime' => 0,
+ 'size' => 0,
+ 'comment' => '',
+ 'entries' => []
+ ];
+ if ( $filename && $this->_unzip( $filename, $is_data ) ) {
+ $this->_parse();
+ }
+ }
+
+ public static function parseFile( $filename, $debug = false ) {
+ return self::parse( $filename, false, $debug );
+ }
+
+ public static function parseData( $data, $debug = false ) {
+ return self::parse( $data, true, $debug );
+ }
+
+ public static function parse( $filename, $is_data = false, $debug = false ) {
+ $xlsx = new self();
+ $xlsx->debug = $debug;
+ if ( $xlsx->_unzip( $filename, $is_data ) ) {
+ $xlsx->_parse();
+ }
+ if ( $xlsx->success() ) {
+ return $xlsx;
+ }
+ self::parseError( $xlsx->error() );
+ self::parseErrno( $xlsx->errno() );
+
+ return false;
+ }
+
+ public static function parseError( $set = false ) {
+ static $error = false;
+
+ return $set ? $error = $set : $error;
+ }
+
+ public static function parseErrno( $set = false ) {
+ static $errno = false;
+
+ return $set ? $errno = $set : $errno;
+ }
+
+ private function _unzip( $filename, $is_data = false ) {
+
+ if ( $is_data ) {
+
+ $this->package['filename'] = 'default.xlsx';
+ $this->package['mtime'] = time();
+ $this->package['size'] = $this->_strlen( $filename );
+
+ $vZ = $filename;
+ } else {
+
+ if ( ! is_readable( $filename ) ) {
+ $this->error( 1, 'File not found ' . $filename );
+
+ return false;
+ }
+
+ // Package information
+ $this->package['filename'] = $filename;
+ $this->package['mtime'] = filemtime( $filename );
+ $this->package['size'] = filesize( $filename );
+
+ // Read file
+ $vZ = file_get_contents( $filename );
+ }
+ // Cut end of central directory
+ /* $aE = explode("\x50\x4b\x05\x06", $vZ);
+
+ if (count($aE) == 1) {
+ $this->error('Unknown format');
+ return false;
+ }
+ */
+ // Explode to each part
+ $aE = explode( "\x50\x4b\x03\x04", $vZ );
+ array_shift( $aE );
+
+ $aEL = count( $aE );
+ if ( $aEL === 0 ) {
+ $this->error( 2, 'Unknown archive format' );
+
+ return false;
+ }
+ // Search central directory end record
+ $last = $aE[ $aEL - 1 ];
+ $last = explode( "\x50\x4b\x05\x06", $last );
+ if ( count( $last ) !== 2 ) {
+ $this->error( 2, 'Unknown archive format' );
+
+ return false;
+ }
+ // Search central directory
+ $last = explode( "\x50\x4b\x01\x02", $last[0] );
+ if ( count( $last ) < 2 ) {
+ $this->error( 2, 'Unknown archive format' );
+
+ return false;
+ }
+ $aE[ $aEL - 1 ] = $last[0];
+
+ // Loop through the entries
+ foreach ( $aE as $vZ ) {
+ $aI = [];
+ $aI['E'] = 0;
+ $aI['EM'] = '';
+ // Retrieving local file header information
+// $aP = unpack('v1VN/v1GPF/v1CM/v1FT/v1FD/V1CRC/V1CS/V1UCS/v1FNL', $vZ);
+ $aP = unpack( 'v1VN/v1GPF/v1CM/v1FT/v1FD/V1CRC/V1CS/V1UCS/v1FNL/v1EFL', $vZ );
+
+ // Check if data is encrypted
+// $bE = ($aP['GPF'] && 0x0001) ? TRUE : FALSE;
+ $bE = false;
+ $nF = $aP['FNL'];
+ $mF = $aP['EFL'];
+
+ // Special case : value block after the compressed data
+ if ( $aP['GPF'] & 0x0008 ) {
+ $aP1 = unpack( 'V1CRC/V1CS/V1UCS', $this->_substr( $vZ, - 12 ) );
+
+ $aP['CRC'] = $aP1['CRC'];
+ $aP['CS'] = $aP1['CS'];
+ $aP['UCS'] = $aP1['UCS'];
+ // 2013-08-10
+ $vZ = $this->_substr( $vZ, 0, - 12 );
+ if ( $this->_substr( $vZ, - 4 ) === "\x50\x4b\x07\x08" ) {
+ $vZ = $this->_substr( $vZ, 0, - 4 );
+ }
+ }
+
+ // Getting stored filename
+ $aI['N'] = $this->_substr( $vZ, 26, $nF );
+ $aI['N'] = str_replace( '\\', '/', $aI['N'] );
+
+ if ( $this->_substr( $aI['N'], - 1 ) === '/' ) {
+ // is a directory entry - will be skipped
+ continue;
+ }
+
+ // Truncate full filename in path and filename
+ $aI['P'] = dirname( $aI['N'] );
+ $aI['P'] = ( $aI['P'] === '.' ) ? '' : $aI['P'];
+ $aI['N'] = basename( $aI['N'] );
+
+ $vZ = $this->_substr( $vZ, 26 + $nF + $mF );
+
+ if ( $this->_strlen( $vZ ) !== (int) $aP['CS'] ) { // check only if availabled
+ $aI['E'] = 1;
+ $aI['EM'] = 'Compressed size is not equal with the value in header information.';
+ } else if ( $bE ) {
+ $aI['E'] = 5;
+ $aI['EM'] = 'File is encrypted, which is not supported from this class.';
+ } else {
+ switch ( $aP['CM'] ) {
+ case 0: // Stored
+ // Here is nothing to do, the file ist flat.
+ break;
+ case 8: // Deflated
+ $vZ = gzinflate( $vZ );
+ break;
+ case 12: // BZIP2
+ if ( extension_loaded( 'bz2' ) ) {
+ /** @noinspection PhpComposerExtensionStubsInspection */
+ $vZ = bzdecompress( $vZ );
+ } else {
+ $aI['E'] = 7;
+ $aI['EM'] = 'PHP BZIP2 extension not available.';
+ }
+ break;
+ default:
+ $aI['E'] = 6;
+ $aI['EM'] = "De-/Compression method {$aP['CM']} is not supported.";
+ }
+ if ( ! $aI['E'] ) {
+ if ( $vZ === false ) {
+ $aI['E'] = 2;
+ $aI['EM'] = 'Decompression of data failed.';
+ } else if ( $this->_strlen( $vZ ) !== (int) $aP['UCS'] ) {
+ $aI['E'] = 3;
+ $aI['EM'] = 'Uncompressed size is not equal with the value in header information.';
+ } else if ( crc32( $vZ ) !== $aP['CRC'] ) {
+ $aI['E'] = 4;
+ $aI['EM'] = 'CRC32 checksum is not equal with the value in header information.';
+ }
+ }
+ }
+
+ $aI['D'] = $vZ;
+
+ // DOS to UNIX timestamp
+ $aI['T'] = mktime( ( $aP['FT'] & 0xf800 ) >> 11,
+ ( $aP['FT'] & 0x07e0 ) >> 5,
+ ( $aP['FT'] & 0x001f ) << 1,
+ ( $aP['FD'] & 0x01e0 ) >> 5,
+ $aP['FD'] & 0x001f,
+ ( ( $aP['FD'] & 0xfe00 ) >> 9 ) + 1980 );
+
+ //$this->Entries[] = &new SimpleUnzipEntry($aI);
+ $this->package['entries'][] = [
+ 'data' => $aI['D'],
+ 'error' => $aI['E'],
+ 'error_msg' => $aI['EM'],
+ 'name' => $aI['N'],
+ 'path' => $aI['P'],
+ 'time' => $aI['T']
+ ];
+
+ } // end for each entries
+
+ return true;
+ }
+
+ // sheets numeration: 1,2,3....
+
+ public function error( $num = null, $str = null ) {
+ if ( $num ) {
+ $this->errno = $num;
+ $this->error = $str;
+ if ( $this->debug ) {
+ trigger_error( __CLASS__ . ': ' . $this->error, E_USER_WARNING );
+ }
+ }
+
+ return $this->error;
+ }
+
+ public function errno() {
+ return $this->errno;
+ }
+
+ private function _parse() {
+ // Document data holders
+ $this->sharedstrings = [];
+ $this->sheets = [];
+// $this->styles = array();
+
+ // Read relations and search for officeDocument
+ if ( $relations = $this->getEntryXML( '_rels/.rels' ) ) {
+
+ foreach ( $relations->Relationship as $rel ) {
+
+ $rel_type = basename( trim( (string) $rel['Type'] ) ); // officeDocument
+ $rel_target = $this->_getTarget( '', (string) $rel['Target'] ); // /xl/workbook.xml or xl/workbook.xml
+
+ if ( $rel_type === 'officeDocument' && $workbook = $this->getEntryXML( $rel_target ) ) {
+
+ $index_rId = []; // [0 => rId1]
+
+ $index = 0;
+ foreach ( $workbook->sheets->sheet as $s ) {
+ $this->sheetNames[ $index ] = (string) $s['name'];
+ $index_rId[ $index ] = (string) $s['id'];
+ $index ++;
+ }
+ if ( (int) $workbook->workbookPr['date1904'] === 1 ) {
+ $this->date1904 = 1;
+ }
+
+
+ if ( $workbookRelations = $this->getEntryXML( dirname( $rel_target ) . '/_rels/workbook.xml.rels' ) ) {
+
+ // Loop relations for workbook and extract sheets...
+ foreach ( $workbookRelations->Relationship as $workbookRelation ) {
+
+ $wrel_type = basename( trim( (string) $workbookRelation['Type'] ) );
+ $wrel_path = $this->_getTarget( dirname( $rel_target ), (string) $workbookRelation['Target'] );
+ if ( ! $this->entryExists( $wrel_path ) ) {
+ continue;
+ }
+
+
+ if ( $wrel_type === 'worksheet' ) { // Sheets
+
+ if ( $sheet = $this->getEntryXML( $wrel_path ) ) {
+ $index = array_search( (string) $workbookRelation['Id'], $index_rId, false );
+ $this->sheets[ $index ] = $sheet;
+ }
+
+ } else if ( $wrel_type === 'sharedStrings' ) {
+
+ if ( $sharedStrings = $this->getEntryXML( $wrel_path ) ) {
+ foreach ( $sharedStrings->si as $val ) {
+ if ( isset( $val->t ) ) {
+ $this->sharedstrings[] = (string) $val->t;
+ } elseif ( isset( $val->r ) ) {
+ $this->sharedstrings[] = $this->_parseRichText( $val );
+ }
+ }
+ }
+ } else if ( $wrel_type === 'styles' ) {
+
+ $this->styles = $this->getEntryXML( $wrel_path );
+
+ $nf = [];
+ if ( $this->styles->numFmts->numFmt !== null ) {
+ foreach ( $this->styles->numFmts->numFmt as $v ) {
+ $nf[ (int) $v['numFmtId'] ] = (string) $v['formatCode'];
+ }
+ }
+
+ if ( $this->styles->cellXfs->xf !== null ) {
+ foreach ( $this->styles->cellXfs->xf as $v ) {
+ $v = (array) $v->attributes();
+ $v['format'] = '';
+
+ if ( isset( $v['@attributes']['numFmtId'] ) ) {
+ $v = $v['@attributes'];
+ $fid = (int) $v['numFmtId'];
+ // formats priority
+ if ( isset( $nf[ $fid ] ) ) {
+ $v['format'] = $nf[ $fid ];
+ } else if ( isset( self::$CF[ $fid ] ) ) {
+ $v['format'] = self::$CF[ $fid ];
+ }
+ }
+ $this->cellFormats[] = $v;
+ }
+ }
+ }
+ }
+
+ break;
+ }
+ }
+ }
+ }
+ if ( count( $this->sheets ) ) {
+ // Sort sheets
+ ksort( $this->sheets );
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /*
+ * @param string $name Filename in archive
+ * @return SimpleXMLElement|bool
+ */
+ public function getEntryXML( $name ) {
+ if ( $entry_xml = $this->getEntryData( $name ) ) {
+ $entry_xml = trim( $entry_xml );
+ // dirty remove namespace prefixes and empty rows
+ $entry_xml = preg_replace( '/xmlns[^=]*="[^"]*"/i', '', $entry_xml ); // remove namespaces
+ $entry_xml = preg_replace( '/[a-zA-Z0-9]+:([a-zA-Z0-9]+="[^"]+")/', '$1$2', $entry_xml ); // remove namespaced attrs
+ $entry_xml = preg_replace( '/<[a-zA-Z0-9]+:([^>]+)>/', '<$1>', $entry_xml ); // fix namespaced openned tags
+ $entry_xml = preg_replace( '/<\/[a-zA-Z0-9]+:([^>]+)>/', '$1>', $entry_xml ); // fix namespaced closed tags
+
+// if ( $this->skipEmptyRows && strpos($name, '/sheet') ) {
+ if ( strpos( $name, '/sheet' ) ) { // dirty skip empty rows
+ $entry_xml = preg_replace( '/]+>\s*( \s*)+<\/row>/', '', $entry_xml, - 1, $cnt ); // remove empty rows
+ $entry_xml = preg_replace( '/
]*\/>/', '', $entry_xml, - 1, $cnt2 );
+ $entry_xml = preg_replace( '/]*><\/row>/', '', $entry_xml, - 1, $cnt3 );
+ if ( $cnt || $cnt2 || $cnt3 ) {
+ $entry_xml = preg_replace( '/ /', '', $entry_xml );
+ }
+// file_put_contents( basename( $name ), $entry_xml ); // @to do comment!!!
+ }
+
+ // XML External Entity (XXE) Prevention
+ $_old = libxml_disable_entity_loader();
+ $entry_xmlobj = simplexml_load_string( $entry_xml );
+
+ libxml_disable_entity_loader( $_old );
+ if ( $entry_xmlobj ) {
+ return $entry_xmlobj;
+ }
+ $e = libxml_get_last_error();
+ $this->error( 3, 'XML-entry ' . $name . ' parser error ' . $e->message . ' line ' . $e->line );
+ } else {
+ $this->error( 4, 'XML-entry not found ' . $name );
+ }
+
+ return false;
+ }
+
+ public function getEntryData( $name ) {
+ $name = ltrim( str_replace( '\\', '/', $name ), '/' );
+ $dir = $this->_strtoupper( dirname( $name ) );
+ $name = $this->_strtoupper( basename( $name ) );
+ foreach ( $this->package['entries'] as $entry ) {
+ if ( $this->_strtoupper( $entry['path'] ) === $dir && $this->_strtoupper( $entry['name'] ) === $name ) {
+ return $entry['data'];
+ }
+ }
+ $this->error( 5, 'Entry not found ' . ( $dir ? $dir . '/' : '' ) . $name );
+
+ return false;
+ }
+
+ public function entryExists( $name ) { // 0.6.6
+ $dir = $this->_strtoupper( dirname( $name ) );
+ $name = $this->_strtoupper( basename( $name ) );
+ foreach ( $this->package['entries'] as $entry ) {
+ if ( $this->_strtoupper( $entry['path'] ) === $dir && $this->_strtoupper( $entry['name'] ) === $name ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private function _parseRichText( $is = null ) {
+ $value = [];
+
+ if ( isset( $is->t ) ) {
+ $value[] = (string) $is->t;
+ } else if ( isset( $is->r ) ) {
+ foreach ( $is->r as $run ) {
+ $value[] = (string) $run->t;
+ }
+ }
+
+ return implode( '', $value );
+ }
+
+ public function success() {
+ return ! $this->error;
+ }
+
+ public function rows( $worksheetIndex = 0 ) {
+
+ if ( ( $ws = $this->worksheet( $worksheetIndex ) ) === false ) {
+ return false;
+ }
+ $dim = $this->dimension( $worksheetIndex );
+ $numCols = $dim[0];
+ $numRows = $dim[1];
+
+ $emptyRow = [];
+ for ( $i = 0; $i < $numCols; $i ++ ) {
+ $emptyRow[] = '';
+ }
+
+ $rows = [];
+ for ( $i = 0; $i < $numRows; $i ++ ) {
+ $rows[] = $emptyRow;
+ }
+
+ $curR = 0;
+ /* @var SimpleXMLElement $ws */
+ foreach ( $ws->sheetData->row as $row ) {
+ $curC = 0;
+ foreach ( $row->c as $c ) {
+ // detect skipped cols
+ $idx = $this->getIndex( (string) $c['r'] );
+ $x = $idx[0];
+ $y = $idx[1];
+
+ if ( $x > - 1 ) {
+ $curC = $x;
+ $curR = $y;
+ }
+
+ $rows[ $curR ][ $curC ] = $this->value( $c );
+ $curC ++;
+ }
+
+ $curR ++;
+ }
+
+ return $rows;
+ }
+
+ public function rowsEx( $worksheetIndex = 0 ) {
+
+ if ( ( $ws = $this->worksheet( $worksheetIndex ) ) === false ) {
+ return false;
+ }
+
+ $rows = [];
+
+ $dim = $this->dimension( $worksheetIndex );
+ $numCols = $dim[0];
+ $numRows = $dim[1];
+
+ for ( $y = 0; $y < $numRows; $y ++ ) {
+ for ( $x = 0; $x < $numCols; $x ++ ) {
+ // 0.6.8
+ $c = '';
+ for ( $k = $x; $k >= 0; $k = (int) ( $k / 26 ) - 1 ) {
+ $c = chr( $k % 26 + 65 ) . $c;
+ }
+ $rows[ $y ][ $x ] = [
+ 'type' => '',
+ 'name' => $c . ( $y + 1 ),
+ 'value' => '',
+ 'href' => '',
+ 'f' => '',
+ 'format' => '',
+ 'r' => $y
+ ];
+ }
+ }
+
+ $curR = 0;
+ /* @var SimpleXMLElement $ws */
+ foreach ( $ws->sheetData->row as $row ) {
+
+ $r_idx = (int) $row['r'];
+ $curC = 0;
+
+ foreach ( $row->c as $c ) {
+ $r = (string) $c['r'];
+ $t = (string) $c['t'];
+ $s = (int) $c['s'];
+
+ $idx = $this->getIndex( $r );
+ $x = $idx[0];
+ $y = $idx[1];
+
+ if ( $x > - 1 ) {
+ $curC = $x;
+ $curR = $y;
+ }
+
+ if ( $s > 0 && isset( $this->cellFormats[ $s ] ) ) {
+ $format = $this->cellFormats[ $s ]['format'];
+ } else {
+ $format = '';
+ }
+
+ $rows[ $curR ][ $curC ] = [
+ 'type' => $t,
+ 'name' => (string) $c['r'],
+ 'value' => $this->value( $c ),
+ 'href' => $this->href( $c ),
+ 'f' => (string) $c->f,
+ 'format' => $format,
+ 'r' => $r_idx
+ ];
+ $curC ++;
+ }
+ $curR ++;
+ }
+
+ return $rows;
+
+ }
+
+ public function toHTML( $worksheetIndex = 0 ) {
+ $s = '';
+ foreach ( $this->rows( $worksheetIndex ) as $r ) {
+ $s .= '';
+ foreach ( $r as $c ) {
+ $s .= '' . ( $c === '' ? ' ' : htmlspecialchars( $c, ENT_QUOTES ) ) . ' ';
+ }
+ $s .= " \r\n";
+ }
+ $s .= '
';
+
+ return $s;
+ }
+
+ public function worksheet( $worksheetIndex = 0 ) {
+
+
+ if ( isset( $this->sheets[ $worksheetIndex ] ) ) {
+ $ws = $this->sheets[ $worksheetIndex ];
+
+ if ( isset( $ws->hyperlinks ) ) {
+ $this->hyperlinks = [];
+ foreach ( $ws->hyperlinks->hyperlink as $hyperlink ) {
+ $this->hyperlinks[ (string) $hyperlink['ref'] ] = (string) $hyperlink['display'];
+ }
+ }
+
+ return $ws;
+ }
+ $this->error( 6, 'Worksheet not found ' . $worksheetIndex );
+
+ return false;
+ }
+
+ /**
+ * returns [numCols,numRows] of worksheet
+ *
+ * @param int $worksheetIndex
+ *
+ * @return array
+ */
+ public function dimension( $worksheetIndex = 0 ) {
+
+ if ( ( $ws = $this->worksheet( $worksheetIndex ) ) === false ) {
+ return [ 0, 0 ];
+ }
+ /* @var SimpleXMLElement $ws */
+
+ $ref = (string) $ws->dimension['ref'];
+
+ if ( $this->_strpos( $ref, ':' ) !== false ) {
+ $d = explode( ':', $ref );
+ $idx = $this->getIndex( $d[1] );
+
+ return [ $idx[0] + 1, $idx[1] + 1 ];
+ }
+ if ( $ref !== '' ) { // 0.6.8
+ $index = $this->getIndex( $ref );
+
+ return [ $index[0] + 1, $index[1] + 1 ];
+ }
+
+ // slow method
+ $maxC = $maxR = 0;
+ foreach ( $ws->sheetData->row as $row ) {
+ foreach ( $row->c as $c ) {
+ $idx = $this->getIndex( (string) $c['r'] );
+ $x = $idx[0];
+ $y = $idx[1];
+ if ( $x > 0 ) {
+ if ( $x > $maxC ) {
+ $maxC = $x;
+ }
+ if ( $y > $maxR ) {
+ $maxR = $y;
+ }
+ }
+ }
+ }
+
+ return [ $maxC + 1, $maxR + 1 ];
+ }
+
+ public function getIndex( $cell = 'A1' ) {
+
+ if ( preg_match( '/([A-Z]+)(\d+)/', $cell, $m ) ) {
+ $col = $m[1];
+ $row = $m[2];
+
+ $colLen = $this->_strlen( $col );
+ $index = 0;
+
+ for ( $i = $colLen - 1; $i >= 0; $i -- ) {
+ /** @noinspection PowerOperatorCanBeUsedInspection */
+ $index += ( ord( $col[ $i ] ) - 64 ) * pow( 26, $colLen - $i - 1 );
+ }
+
+ return [ $index - 1, $row - 1 ];
+ }
+
+// $this->error( 'Invalid cell index ' . $cell );
+
+ return [ - 1, - 1 ];
+ }
+
+ public function value( $cell ) {
+ // Determine data type
+ $dataType = (string) $cell['t'];
+
+ if ( $dataType === '' || $dataType === 'n' ) { // number
+ $s = (int) $cell['s'];
+ if ( $s > 0 && isset( $this->cellFormats[ $s ] ) ) {
+ $format = $this->cellFormats[ $s ]['format'];
+ if ( preg_match( '/[mM]/', $format ) ) { // [m]onth
+ $dataType = 'd';
+ }
+ }
+ }
+
+ $value = '';
+
+ switch ( $dataType ) {
+ case 's':
+ // Value is a shared string
+ if ( (string) $cell->v !== '' ) {
+ $value = $this->sharedstrings[ (int) $cell->v ];
+ }
+
+ break;
+
+ case 'b':
+ // Value is boolean
+ $value = (string) $cell->v;
+ if ( $value === '0' ) {
+ $value = false;
+ } else if ( $value === '1' ) {
+ $value = true;
+ } else {
+ $value = (bool) $cell->v;
+ }
+
+ break;
+
+ case 'inlineStr':
+ // Value is rich text inline
+ $value = $this->_parseRichText( $cell->is );
+
+ break;
+
+ case 'e':
+ // Value is an error message
+ if ( (string) $cell->v !== '' ) {
+ $value = (string) $cell->v;
+ }
+
+ break;
+ case 'd':
+ // Value is a date and non-empty
+ if ( ! empty( $cell->v ) ) {
+ $value = $this->datetimeFormat ? gmdate( $this->datetimeFormat, $this->unixstamp( (float) $cell->v ) ) : (float) $cell->v;
+ }
+ break;
+
+
+ default:
+ // Value is a string
+ $value = (string) $cell->v;
+
+ // Check for numeric values
+ if ( is_numeric( $value ) && $dataType !== 's' ) {
+ /** @noinspection TypeUnsafeComparisonInspection */
+ if ( $value == (int) $value ) {
+ $value = (int) $value;
+ } /** @noinspection TypeUnsafeComparisonInspection */ elseif ( $value == (float) $value ) {
+ $value = (float) $value;
+ }
+ }
+ }
+
+ return $value;
+ }
+
+ public function unixstamp( $excelDateTime ) {
+
+ $d = floor( $excelDateTime ); // days since 1900 or 1904
+ $t = $excelDateTime - $d;
+
+ if ( $this->date1904 ) {
+ $d += 1462;
+ }
+
+ $t = ( abs( $d ) > 0 ) ? ( $d - 25569 ) * 86400 + round( $t * 86400 ) : round( $t * 86400 );
+
+ return (int) $t;
+ }
+
+ /**
+ * Returns cell value
+ * VERY SLOW! Use ->rows() or ->rowsEx()
+ *
+ * @param int $worksheetIndex
+ * @param string|array $cell ref or coords, D12 or [3,12]
+ *
+ * @return mixed Returns NULL if not found
+ */
+ public function getCell( $worksheetIndex = 0, $cell = 'A1' ) {
+
+ if ( ( $ws = $this->worksheet( $worksheetIndex ) ) === false ) {
+ return false;
+ }
+
+ $idx = is_array( $cell ) ? $cell : $this->getIndex( (string) $cell );
+ $C = $idx[0];
+ $R = $idx[1];
+
+ $curR = 0;
+ /* @var SimpleXMLElement $ws */
+ foreach ( $ws->sheetData->row as $row ) {
+ $curC = 0;
+ foreach ( $row->c as $c ) {
+ // detect skipped cols
+ $idx = $this->getIndex( (string) $c['r'] );
+ $x = $idx[0];
+ $y = $idx[1];
+ if ( $x > 0 ) {
+ $curC = $x;
+ $curR = $y;
+ }
+ if ( $curR === $R && $curC === $C ) {
+ return $this->value( $c );
+ }
+ if ( $curR > $R ) {
+ return null;
+ }
+ $curC ++;
+ }
+
+ $curR ++;
+ }
+
+ return null;
+ }
+
+ public function href( $cell ) {
+ return isset( $this->hyperlinks[ (string) $cell['r'] ] ) ? $this->hyperlinks[ (string) $cell['r'] ] : '';
+ }
+
+ public function sheets() {
+ return $this->sheets;
+ }
+
+ public function sheetsCount() {
+ return count( $this->sheets );
+ }
+
+ public function sheetName( $worksheetIndex ) {
+ if ( isset( $this->sheetNames[ $worksheetIndex ] ) ) {
+ return $this->sheetNames[ $worksheetIndex ];
+ }
+
+ return false;
+ }
+
+ public function sheetNames() {
+
+ return $this->sheetNames;
+ }
+
+ // thx Gonzo
+
+ public function getStyles() {
+ return $this->styles;
+ }
+
+ public function getPackage() {
+ return $this->package;
+ }
+
+ public function setDateTimeFormat( $value ) {
+ $this->datetimeFormat = is_string( $value ) ? $value : false;
+ }
+
+ private function _strlen( $str ) {
+ return ( ini_get( 'mbstring.func_overload' ) & 2 ) ? mb_strlen( $str, '8bit' ) : strlen( $str );
+ }
+
+ private function _strpos( $haystack, $needle, $offset = 0 ) {
+ return ( ini_get( 'mbstring.func_overload' ) & 2 ) ? mb_strpos( $haystack, $needle, $offset, '8bit' ) : strpos( $haystack, $needle, $offset );
+ }
+
+ /*
+ private function _strrpos( $haystack, $needle, $offset = 0 ) {
+ return (ini_get('mbstring.func_overload') & 2) ? mb_strrpos( $haystack, $needle, $offset, '8bit') : strrpos($haystack, $needle, $offset);
+ }*/
+ private function _strtoupper( $str ) {
+ return ( ini_get( 'mbstring.func_overload' ) & 2 ) ? mb_strtoupper( $str, '8bit' ) : strtoupper( $str );
+ }
+
+ private function _substr( $str, $start, $length = null ) {
+ return ( ini_get( 'mbstring.func_overload' ) & 2 ) ? mb_substr( $str, $start, ( $length === null ) ? mb_strlen( $str, '8bit' ) : $length, '8bit' ) : substr( $str, $start, ( $length === null ) ? strlen( $str ) : $length );
+ }
+
+ private function _getTarget( $base, $target ) {
+ $target = trim( $target );
+ if ( strpos( $target, '/' ) === 0 ) {
+ return $this->_substr( $target, 1 );
+ }
+ $target = ( $base ? $base . '/' : '' ) . $target;
+ // a/b/../c -> a/c
+ $parts = explode( '/', $target );
+ $abs = [];
+ foreach ( $parts as $p ) {
+ if ( '.' === $p ) {
+ continue;
+ }
+ if ( '..' === $p ) {
+ array_pop( $abs );
+ } else {
+ $abs[] = $p;
+ }
+ }
+ return implode( '/', $abs );
+ }
+
+}
\ No newline at end of file
diff --git a/modules/import_api/logo.png b/modules/import_api/logo.png
new file mode 100644
index 00000000..5768be96
Binary files /dev/null and b/modules/import_api/logo.png differ
diff --git a/modules/import_api/sql_install.php b/modules/import_api/sql_install.php
new file mode 100644
index 00000000..966a14e1
--- /dev/null
+++ b/modules/import_api/sql_install.php
@@ -0,0 +1,90 @@
+
+ * @copyright 2019 Dalibor Stojcevski
+ * @license Dalibor Stojcevski
+ */
+
+//$sql = array();
+$sql[_DB_PREFIX_.'ia_files'] = 'CREATE TABLE IF NOT EXISTS `'._DB_PREFIX_.'ia_files` (
+ `file_id` int(11) NOT NULL AUTO_INCREMENT,
+ `name` varchar(200) NOT NULL,
+ `link` varchar(300) NOT NULL,
+ `shop` varchar(200) NOT NULL,
+ `source` varchar(10) NOT NULL,
+ `headers` smallint(2) NOT NULL DEFAULT \'0\',
+ `status` smallint(2) NOT NULL DEFAULT \'1\',
+
+ `delimiter` varchar(16) NOT NULL,
+ `mime_type` varchar(100) NOT NULL,
+ `fields` text NOT NULL,
+ `date_added` int(11) NOT NULL,
+ `mask` varchar(30) NOT NULL,
+ `post` text,
+ `date_edited` int(11) NOT NULL DEFAULT \'0\',
+ PRIMARY KEY (`file_id`)
+) ENGINE='._MYSQL_ENGINE_.' DEFAULT CHARSET=utf8;';
+
+$sql[_DB_PREFIX_.'ia_file_settings'] = 'CREATE TABLE IF NOT EXISTS `'._DB_PREFIX_.'ia_file_settings` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `mapping` text NOT NULL,
+ `modification` text NOT NULL,
+ `split` text NOT NULL,
+ `filter` text NOT NULL,
+ `filter_options` text NOT NULL,
+ `replace` text NOT NULL,
+ `settings` text NOT NULL,
+ `shop` varchar(300) NOT NULL,
+ `file_id` int(11) NOT NULL,
+ `date_updated` int(11) NOT NULL,
+ PRIMARY KEY (`id`)
+) ENGINE='._MYSQL_ENGINE_.' DEFAULT CHARSET=utf8;';
+
+$sql[_DB_PREFIX_.'ia_temp'] = 'CREATE TABLE IF NOT EXISTS `'._DB_PREFIX_.'ia_temp` (
+ `product_id` BigInt(20) NOT NULL AUTO_INCREMENT,
+ `id` varchar(30) NOT NULL,
+ `source_id` varchar(200) NOT NULL,
+ `indx` varchar(300) NOT NULL,
+ `shop` varchar(64) NOT NULL,
+ `product` text NOT NULL,
+ `date_added` int(11) NOT NULL,
+ `queue_id` int(11) NOT NULL,
+ `file_id` int(11) NOT NULL DEFAULT \'0\',
+ PRIMARY KEY (`product_id`)
+) ENGINE='._MYSQL_ENGINE_.' DEFAULT CHARSET=utf8;';
+
+$sql[_DB_PREFIX_.'ia_queues'] = 'CREATE TABLE IF NOT EXISTS `'._DB_PREFIX_.'ia_queues` (
+ `queue_id` int(11) NOT NULL AUTO_INCREMENT,
+ `total` int(11) NOT NULL,
+ `date_added` int(11) NOT NULL,
+ `shop` varchar(300) NOT NULL,
+ `status` int(11) NOT NULL DEFAULT \'0\',
+ `date_processed` int(11) NOT NULL DEFAULT \'0\',
+ `products_created` int(11) NOT NULL DEFAULT \'0\',
+ `products_updated` int(11) NOT NULL DEFAULT \'0\',
+ `products_failed` int(11) NOT NULL DEFAULT \'0\',
+ `source` varchar(10) NOT NULL DEFAULT \'n\',
+ `wait_time` int(11) NOT NULL DEFAULT \'0\',
+ `file_id` int(11) NOT NULL DEFAULT \'0\',
+ PRIMARY KEY (`queue_id`)
+) ENGINE='._MYSQL_ENGINE_.' DEFAULT CHARSET=utf8;';
+
+$sql[_DB_PREFIX_.'ia_products'] = 'CREATE TABLE IF NOT EXISTS `'._DB_PREFIX_.'ia_products` (
+ `product_id`int(11) NOT NULL,
+ `indx` varchar(300) NOT NULL,
+ `shop` varchar(64) NOT NULL,
+ `product` text NOT NULL,
+ `date_edited` int(11) NOT NULL,
+ `source` varchar(30) NOT NULL,
+ `date_added` int(11) NOT NULL DEFAULT \'0\',
+ `file_id` int(11) NOT NULL DEFAULT \'0\',
+ `queue_id` int(11) NOT NULL DEFAULT \'0\',
+ `missing` int(11) NOT NULL DEFAULT \'0\',
+ PRIMARY KEY (`product_id`)
+) ENGINE='._MYSQL_ENGINE_.' DEFAULT CHARSET=utf8;';
\ No newline at end of file
diff --git a/modules/import_api/translations/cs.php b/modules/import_api/translations/cs.php
new file mode 100644
index 00000000..e69de29b
diff --git a/modules/import_api/translations/pl.php b/modules/import_api/translations/pl.php
new file mode 100644
index 00000000..e69de29b
diff --git a/modules/import_api/views/css/dataTables.bootstrap4.css b/modules/import_api/views/css/dataTables.bootstrap4.css
new file mode 100644
index 00000000..84ec2036
--- /dev/null
+++ b/modules/import_api/views/css/dataTables.bootstrap4.css
@@ -0,0 +1,206 @@
+table.dataTable {
+ clear: both;
+ margin-top: 6px !important;
+ margin-bottom: 6px !important;
+ max-width: none !important;
+ border-collapse: separate !important;
+ border-spacing: 0;
+}
+table.dataTable td,
+table.dataTable th {
+ -webkit-box-sizing: content-box;
+ box-sizing: content-box;
+}
+table.dataTable td.dataTables_empty,
+table.dataTable th.dataTables_empty {
+ text-align: center;
+}
+table.dataTable.nowrap th,
+table.dataTable.nowrap td {
+ white-space: nowrap;
+}
+
+div.dataTables_wrapper div.dataTables_length label {
+ font-weight: normal;
+ text-align: left;
+ white-space: nowrap;
+}
+div.dataTables_wrapper div.dataTables_length select {
+ width: auto;
+ display: inline-block;
+}
+div.dataTables_wrapper div.dataTables_filter {
+ text-align: right;
+}
+div.dataTables_wrapper div.dataTables_filter label {
+ font-weight: normal;
+ white-space: nowrap;
+ text-align: left;
+}
+div.dataTables_wrapper div.dataTables_filter input {
+ margin-left: 0.5em;
+ display: inline-block;
+ width: auto;
+}
+div.dataTables_wrapper div.dataTables_info {
+ padding-top: 0.85em;
+ white-space: nowrap;
+}
+div.dataTables_wrapper div.dataTables_paginate {
+ margin: 0;
+ white-space: nowrap;
+ text-align: right;
+}
+div.dataTables_wrapper div.dataTables_paginate ul.pagination {
+ margin: 2px 0;
+ white-space: nowrap;
+ justify-content: flex-end;
+}
+div.dataTables_wrapper div.dataTables_processing {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ width: 200px;
+ margin-left: -100px;
+ margin-top: -26px;
+ text-align: center;
+ padding: 1em 0;
+}
+
+table.dataTable thead > tr > th.sorting_asc, table.dataTable thead > tr > th.sorting_desc, table.dataTable thead > tr > th.sorting,
+table.dataTable thead > tr > td.sorting_asc,
+table.dataTable thead > tr > td.sorting_desc,
+table.dataTable thead > tr > td.sorting {
+ padding-right: 30px;
+}
+table.dataTable thead > tr > th:active,
+table.dataTable thead > tr > td:active {
+ outline: none;
+}
+table.dataTable thead .sorting,
+table.dataTable thead .sorting_asc,
+table.dataTable thead .sorting_desc,
+table.dataTable thead .sorting_asc_disabled,
+table.dataTable thead .sorting_desc_disabled {
+ cursor: pointer;
+ position: relative;
+}
+table.dataTable thead .sorting:before, table.dataTable thead .sorting:after,
+table.dataTable thead .sorting_asc:before,
+table.dataTable thead .sorting_asc:after,
+table.dataTable thead .sorting_desc:before,
+table.dataTable thead .sorting_desc:after,
+table.dataTable thead .sorting_asc_disabled:before,
+table.dataTable thead .sorting_asc_disabled:after,
+table.dataTable thead .sorting_desc_disabled:before,
+table.dataTable thead .sorting_desc_disabled:after {
+ position: absolute;
+ bottom: 0.9em;
+ display: block;
+ opacity: 0.3;
+}
+table.dataTable thead .sorting:before,
+table.dataTable thead .sorting_asc:before,
+table.dataTable thead .sorting_desc:before,
+table.dataTable thead .sorting_asc_disabled:before,
+table.dataTable thead .sorting_desc_disabled:before {
+ right: 1em;
+ content: "\2191";
+}
+table.dataTable thead .sorting:after,
+table.dataTable thead .sorting_asc:after,
+table.dataTable thead .sorting_desc:after,
+table.dataTable thead .sorting_asc_disabled:after,
+table.dataTable thead .sorting_desc_disabled:after {
+ right: 0.5em;
+ content: "\2193";
+}
+table.dataTable thead .sorting_asc:before,
+table.dataTable thead .sorting_desc:after {
+ opacity: 1;
+}
+table.dataTable thead .sorting_asc_disabled:before,
+table.dataTable thead .sorting_desc_disabled:after {
+ opacity: 0;
+}
+
+div.dataTables_scrollHead table.dataTable {
+ margin-bottom: 0 !important;
+}
+
+div.dataTables_scrollBody table {
+ border-top: none;
+ margin-top: 0 !important;
+ margin-bottom: 0 !important;
+}
+div.dataTables_scrollBody table thead .sorting:before,
+div.dataTables_scrollBody table thead .sorting_asc:before,
+div.dataTables_scrollBody table thead .sorting_desc:before,
+div.dataTables_scrollBody table thead .sorting:after,
+div.dataTables_scrollBody table thead .sorting_asc:after,
+div.dataTables_scrollBody table thead .sorting_desc:after {
+ display: none;
+}
+div.dataTables_scrollBody table tbody tr:first-child th,
+div.dataTables_scrollBody table tbody tr:first-child td {
+ border-top: none;
+}
+
+div.dataTables_scrollFoot > .dataTables_scrollFootInner {
+ box-sizing: content-box;
+}
+div.dataTables_scrollFoot > .dataTables_scrollFootInner > table {
+ margin-top: 0 !important;
+ border-top: none;
+}
+
+@media screen and (max-width: 767px) {
+ div.dataTables_wrapper div.dataTables_length,
+ div.dataTables_wrapper div.dataTables_filter,
+ div.dataTables_wrapper div.dataTables_info,
+ div.dataTables_wrapper div.dataTables_paginate {
+ text-align: center;
+ }
+}
+table.dataTable.table-sm > thead > tr > th {
+ padding-right: 20px;
+}
+table.dataTable.table-sm .sorting:before,
+table.dataTable.table-sm .sorting_asc:before,
+table.dataTable.table-sm .sorting_desc:before {
+ top: 5px;
+ right: 0.85em;
+}
+table.dataTable.table-sm .sorting:after,
+table.dataTable.table-sm .sorting_asc:after,
+table.dataTable.table-sm .sorting_desc:after {
+ top: 5px;
+}
+
+table.table-bordered.dataTable th,
+table.table-bordered.dataTable td {
+ border-left-width: 0;
+}
+table.table-bordered.dataTable th:last-child, table.table-bordered.dataTable th:last-child,
+table.table-bordered.dataTable td:last-child,
+table.table-bordered.dataTable td:last-child {
+ border-right-width: 0;
+}
+table.table-bordered.dataTable tbody th,
+table.table-bordered.dataTable tbody td {
+ border-bottom-width: 0;
+}
+
+div.dataTables_scrollHead table.table-bordered {
+ border-bottom-width: 0;
+}
+
+div.table-responsive > div.dataTables_wrapper > div.row {
+ margin: 0;
+}
+div.table-responsive > div.dataTables_wrapper > div.row > div[class^="col-"]:first-child {
+ padding-left: 0;
+}
+div.table-responsive > div.dataTables_wrapper > div.row > div[class^="col-"]:last-child {
+ padding-right: 0;
+}
diff --git a/modules/import_api/views/index.php b/modules/import_api/views/index.php
new file mode 100644
index 00000000..907720c1
--- /dev/null
+++ b/modules/import_api/views/index.php
@@ -0,0 +1,35 @@
+
+* @copyright 2007-2018 PrestaShop SA
+* @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0)
+* International Registered Trademark & Property of PrestaShop SA
+*/
+
+header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
+header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT');
+
+header('Cache-Control: no-store, no-cache, must-revalidate');
+header('Cache-Control: post-check=0, pre-check=0', false);
+header('Pragma: no-cache');
+
+header('Location: ../');
+exit;
diff --git a/modules/import_api/views/js/back.js b/modules/import_api/views/js/back.js
new file mode 100644
index 00000000..77935cba
--- /dev/null
+++ b/modules/import_api/views/js/back.js
@@ -0,0 +1,27 @@
+/**
+* 2007-2018 PrestaShop
+*
+* NOTICE OF LICENSE
+*
+* This source file is subject to the Academic Free License (AFL 3.0)
+* that is bundled with this package in the file LICENSE.txt.
+* It is also available through the world-wide-web at this URL:
+* http://opensource.org/licenses/afl-3.0.php
+* If you did not receive a copy of the license and are unable to
+* obtain it through the world-wide-web, please send an email
+* to license@prestashop.com so we can send you a copy immediately.
+*
+* DISCLAIMER
+*
+* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
+* versions in the future. If you wish to customize PrestaShop for your
+* needs please refer to http://www.prestashop.com for more information.
+*
+* @author PrestaShop SA
+* @copyright 2007-2018 PrestaShop SA
+* @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0)
+* International Registered Trademark & Property of PrestaShop SA
+*
+* Don't forget to prefix your containers with your own identifier
+* to avoid any conflicts with others containers.
+*/
diff --git a/modules/import_api/views/js/dataTables.bootstrap4.js b/modules/import_api/views/js/dataTables.bootstrap4.js
new file mode 100644
index 00000000..f2d2ad58
--- /dev/null
+++ b/modules/import_api/views/js/dataTables.bootstrap4.js
@@ -0,0 +1,184 @@
+/*! DataTables Bootstrap 4 integration
+ * ©2011-2017 SpryMedia Ltd - datatables.net/license
+ */
+
+/**
+ * DataTables integration for Bootstrap 4. This requires Bootstrap 4 and
+ * DataTables 1.10 or newer.
+ *
+ * This file sets the defaults and adds options to DataTables to style its
+ * controls using Bootstrap. See http://datatables.net/manual/styling/bootstrap
+ * for further information.
+ */
+(function( factory ){
+ if ( typeof define === 'function' && define.amd ) {
+ // AMD
+ define( ['jquery', 'datatables.net'], function ( $ ) {
+ return factory( $, window, document );
+ } );
+ }
+ else if ( typeof exports === 'object' ) {
+ // CommonJS
+ module.exports = function (root, $) {
+ if ( ! root ) {
+ root = window;
+ }
+
+ if ( ! $ || ! $.fn.dataTable ) {
+ // Require DataTables, which attaches to jQuery, including
+ // jQuery if needed and have a $ property so we can access the
+ // jQuery object that is used
+ $ = require('datatables.net')(root, $).$;
+ }
+
+ return factory( $, root, root.document );
+ };
+ }
+ else {
+ // Browser
+ factory( jQuery, window, document );
+ }
+}(function( $, window, document, undefined ) {
+'use strict';
+var DataTable = $.fn.dataTable;
+
+
+/* Set the defaults for DataTables initialisation */
+$.extend( true, DataTable.defaults, {
+ dom:
+ "<'row'<'col-sm-12 col-md-6'l><'col-sm-12 col-md-6'f>>" +
+ "<'row'<'col-sm-12'tr>>" +
+ "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
+ renderer: 'bootstrap'
+} );
+
+
+/* Default class modification */
+$.extend( DataTable.ext.classes, {
+ sWrapper: "dataTables_wrapper dt-bootstrap4",
+ sFilterInput: "form-control form-control-sm",
+ sLengthSelect: "custom-select custom-select-sm form-control form-control-sm",
+ sProcessing: "dataTables_processing card",
+ sPageButton: "paginate_button page-item"
+} );
+
+
+/* Bootstrap paging button renderer */
+DataTable.ext.renderer.pageButton.bootstrap = function ( settings, host, idx, buttons, page, pages ) {
+ var api = new DataTable.Api( settings );
+ var classes = settings.oClasses;
+ var lang = settings.oLanguage.oPaginate;
+ var aria = settings.oLanguage.oAria.paginate || {};
+ var btnDisplay, btnClass, counter=0;
+
+ var attach = function( container, buttons ) {
+ var i, ien, node, button;
+ var clickHandler = function ( e ) {
+ e.preventDefault();
+ if ( !$(e.currentTarget).hasClass('disabled') && api.page() != e.data.action ) {
+ api.page( e.data.action ).draw( 'page' );
+ }
+ };
+
+ for ( i=0, ien=buttons.length ; i 0 ?
+ '' : ' disabled');
+ break;
+
+ case 'previous':
+ btnDisplay = lang.sPrevious;
+ btnClass = button + (page > 0 ?
+ '' : ' disabled');
+ break;
+
+ case 'next':
+ btnDisplay = lang.sNext;
+ btnClass = button + (page < pages-1 ?
+ '' : ' disabled');
+ break;
+
+ case 'last':
+ btnDisplay = lang.sLast;
+ btnClass = button + (page < pages-1 ?
+ '' : ' disabled');
+ break;
+
+ default:
+ btnDisplay = button + 1;
+ btnClass = page === button ?
+ 'active' : '';
+ break;
+ }
+
+ if ( btnDisplay ) {
+ node = $('', {
+ 'class': classes.sPageButton+' '+btnClass,
+ 'id': idx === 0 && typeof button === 'string' ?
+ settings.sTableId +'_'+ button :
+ null
+ } )
+ .append( $('', {
+ 'href': '#',
+ 'aria-controls': settings.sTableId,
+ 'aria-label': aria[ button ],
+ 'data-dt-idx': counter,
+ 'tabindex': settings.iTabIndex,
+ 'class': 'page-link'
+ } )
+ .html( btnDisplay )
+ )
+ .appendTo( container );
+
+ settings.oApi._fnBindAction(
+ node, {action: button}, clickHandler
+ );
+
+ counter++;
+ }
+ }
+ }
+ };
+
+ // IE9 throws an 'unknown error' if document.activeElement is used
+ // inside an iframe or frame.
+ var activeEl;
+
+ try {
+ // Because this approach is destroying and recreating the paging
+ // elements, focus is lost on the select button which is bad for
+ // accessibility. So we want to restore focus once the draw has
+ // completed
+ activeEl = $(host).find(document.activeElement).data('dt-idx');
+ }
+ catch (e) {}
+
+ attach(
+ $(host).empty().html('').children('ul'),
+ buttons
+ );
+
+ if ( activeEl !== undefined ) {
+ $(host).find( '[data-dt-idx='+activeEl+']' ).focus();
+ }
+};
+
+
+return DataTable;
+}));
diff --git a/modules/import_api/views/js/dataTables.js b/modules/import_api/views/js/dataTables.js
new file mode 100644
index 00000000..7f2cb5f1
--- /dev/null
+++ b/modules/import_api/views/js/dataTables.js
@@ -0,0 +1,15334 @@
+/*! DataTables 1.10.20
+ * ©2008-2019 SpryMedia Ltd - datatables.net/license
+ */
+
+/**
+ * @summary DataTables
+ * @description Paginate, search and order HTML tables
+ * @version 1.10.20
+ * @file jquery.dataTables.js
+ * @author SpryMedia Ltd
+ * @contact www.datatables.net
+ * @copyright Copyright 2008-2019 SpryMedia Ltd.
+ *
+ * This source file is free software, available under the following license:
+ * MIT license - http://datatables.net/license
+ *
+ * This source file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the license files for details.
+ *
+ * For details please refer to: http://www.datatables.net
+ */
+
+/*jslint evil: true, undef: true, browser: true */
+/*globals $,require,jQuery,define,_selector_run,_selector_opts,_selector_first,_selector_row_indexes,_ext,_Api,_api_register,_api_registerPlural,_re_new_lines,_re_html,_re_formatted_numeric,_re_escape_regex,_empty,_intVal,_numToDecimal,_isNumber,_isHtml,_htmlNumeric,_pluck,_pluck_order,_range,_stripHtml,_unique,_fnBuildAjax,_fnAjaxUpdate,_fnAjaxParameters,_fnAjaxUpdateDraw,_fnAjaxDataSrc,_fnAddColumn,_fnColumnOptions,_fnAdjustColumnSizing,_fnVisibleToColumnIndex,_fnColumnIndexToVisible,_fnVisbleColumns,_fnGetColumns,_fnColumnTypes,_fnApplyColumnDefs,_fnHungarianMap,_fnCamelToHungarian,_fnLanguageCompat,_fnBrowserDetect,_fnAddData,_fnAddTr,_fnNodeToDataIndex,_fnNodeToColumnIndex,_fnGetCellData,_fnSetCellData,_fnSplitObjNotation,_fnGetObjectDataFn,_fnSetObjectDataFn,_fnGetDataMaster,_fnClearTable,_fnDeleteIndex,_fnInvalidate,_fnGetRowElements,_fnCreateTr,_fnBuildHead,_fnDrawHead,_fnDraw,_fnReDraw,_fnAddOptionsHtml,_fnDetectHeader,_fnGetUniqueThs,_fnFeatureHtmlFilter,_fnFilterComplete,_fnFilterCustom,_fnFilterColumn,_fnFilter,_fnFilterCreateSearch,_fnEscapeRegex,_fnFilterData,_fnFeatureHtmlInfo,_fnUpdateInfo,_fnInfoMacros,_fnInitialise,_fnInitComplete,_fnLengthChange,_fnFeatureHtmlLength,_fnFeatureHtmlPaginate,_fnPageChange,_fnFeatureHtmlProcessing,_fnProcessingDisplay,_fnFeatureHtmlTable,_fnScrollDraw,_fnApplyToChildren,_fnCalculateColumnWidths,_fnThrottle,_fnConvertToWidth,_fnGetWidestNode,_fnGetMaxLenString,_fnStringToCss,_fnSortFlatten,_fnSort,_fnSortAria,_fnSortListener,_fnSortAttachListener,_fnSortingClasses,_fnSortData,_fnSaveState,_fnLoadState,_fnSettingsFromNode,_fnLog,_fnMap,_fnBindAction,_fnCallbackReg,_fnCallbackFire,_fnLengthOverflow,_fnRenderer,_fnDataSource,_fnRowAttributes*/
+
+(function( factory ) {
+ "use strict";
+
+ if ( typeof define === 'function' && define.amd ) {
+ // AMD
+ define( ['jquery'], function ( $ ) {
+ return factory( $, window, document );
+ } );
+ }
+ else if ( typeof exports === 'object' ) {
+ // CommonJS
+ module.exports = function (root, $) {
+ if ( ! root ) {
+ // CommonJS environments without a window global must pass a
+ // root. This will give an error otherwise
+ root = window;
+ }
+
+ if ( ! $ ) {
+ $ = typeof window !== 'undefined' ? // jQuery's factory checks for a global window
+ require('jquery') :
+ require('jquery')( root );
+ }
+
+ return factory( $, root, root.document );
+ };
+ }
+ else {
+ // Browser
+ factory( jQuery, window, document );
+ }
+}
+(function( $, window, document, undefined ) {
+ "use strict";
+
+ /**
+ * DataTables is a plug-in for the jQuery Javascript library. It is a highly
+ * flexible tool, based upon the foundations of progressive enhancement,
+ * which will add advanced interaction controls to any HTML table. For a
+ * full list of features please refer to
+ * [DataTables.net](href="http://datatables.net).
+ *
+ * Note that the `DataTable` object is not a global variable but is aliased
+ * to `jQuery.fn.DataTable` and `jQuery.fn.dataTable` through which it may
+ * be accessed.
+ *
+ * @class
+ * @param {object} [init={}] Configuration object for DataTables. Options
+ * are defined by {@link DataTable.defaults}
+ * @requires jQuery 1.7+
+ *
+ * @example
+ * // Basic initialisation
+ * $(document).ready( function {
+ * $('#example').dataTable();
+ * } );
+ *
+ * @example
+ * // Initialisation with configuration options - in this case, disable
+ * // pagination and sorting.
+ * $(document).ready( function {
+ * $('#example').dataTable( {
+ * "paginate": false,
+ * "sort": false
+ * } );
+ * } );
+ */
+ var DataTable = function ( options )
+ {
+ /**
+ * Perform a jQuery selector action on the table's TR elements (from the tbody) and
+ * return the resulting jQuery object.
+ * @param {string|node|jQuery} sSelector jQuery selector or node collection to act on
+ * @param {object} [oOpts] Optional parameters for modifying the rows to be included
+ * @param {string} [oOpts.filter=none] Select TR elements that meet the current filter
+ * criterion ("applied") or all TR elements (i.e. no filter).
+ * @param {string} [oOpts.order=current] Order of the TR elements in the processed array.
+ * Can be either 'current', whereby the current sorting of the table is used, or
+ * 'original' whereby the original order the data was read into the table is used.
+ * @param {string} [oOpts.page=all] Limit the selection to the currently displayed page
+ * ("current") or not ("all"). If 'current' is given, then order is assumed to be
+ * 'current' and filter is 'applied', regardless of what they might be given as.
+ * @returns {object} jQuery object, filtered by the given selector.
+ * @dtopt API
+ * @deprecated Since v1.10
+ *
+ * @example
+ * $(document).ready(function() {
+ * var oTable = $('#example').dataTable();
+ *
+ * // Highlight every second row
+ * oTable.$('tr:odd').css('backgroundColor', 'blue');
+ * } );
+ *
+ * @example
+ * $(document).ready(function() {
+ * var oTable = $('#example').dataTable();
+ *
+ * // Filter to rows with 'Webkit' in them, add a background colour and then
+ * // remove the filter, thus highlighting the 'Webkit' rows only.
+ * oTable.fnFilter('Webkit');
+ * oTable.$('tr', {"search": "applied"}).css('backgroundColor', 'blue');
+ * oTable.fnFilter('');
+ * } );
+ */
+ this.$ = function ( sSelector, oOpts )
+ {
+ return this.api(true).$( sSelector, oOpts );
+ };
+
+
+ /**
+ * Almost identical to $ in operation, but in this case returns the data for the matched
+ * rows - as such, the jQuery selector used should match TR row nodes or TD/TH cell nodes
+ * rather than any descendants, so the data can be obtained for the row/cell. If matching
+ * rows are found, the data returned is the original data array/object that was used to
+ * create the row (or a generated array if from a DOM source).
+ *
+ * This method is often useful in-combination with $ where both functions are given the
+ * same parameters and the array indexes will match identically.
+ * @param {string|node|jQuery} sSelector jQuery selector or node collection to act on
+ * @param {object} [oOpts] Optional parameters for modifying the rows to be included
+ * @param {string} [oOpts.filter=none] Select elements that meet the current filter
+ * criterion ("applied") or all elements (i.e. no filter).
+ * @param {string} [oOpts.order=current] Order of the data in the processed array.
+ * Can be either 'current', whereby the current sorting of the table is used, or
+ * 'original' whereby the original order the data was read into the table is used.
+ * @param {string} [oOpts.page=all] Limit the selection to the currently displayed page
+ * ("current") or not ("all"). If 'current' is given, then order is assumed to be
+ * 'current' and filter is 'applied', regardless of what they might be given as.
+ * @returns {array} Data for the matched elements. If any elements, as a result of the
+ * selector, were not TR, TD or TH elements in the DataTable, they will have a null
+ * entry in the array.
+ * @dtopt API
+ * @deprecated Since v1.10
+ *
+ * @example
+ * $(document).ready(function() {
+ * var oTable = $('#example').dataTable();
+ *
+ * // Get the data from the first row in the table
+ * var data = oTable._('tr:first');
+ *
+ * // Do something useful with the data
+ * alert( "First cell is: "+data[0] );
+ * } );
+ *
+ * @example
+ * $(document).ready(function() {
+ * var oTable = $('#example').dataTable();
+ *
+ * // Filter to 'Webkit' and get all data for
+ * oTable.fnFilter('Webkit');
+ * var data = oTable._('tr', {"search": "applied"});
+ *
+ * // Do something with the data
+ * alert( data.length+" rows matched the search" );
+ * } );
+ */
+ this._ = function ( sSelector, oOpts )
+ {
+ return this.api(true).rows( sSelector, oOpts ).data();
+ };
+
+
+ /**
+ * Create a DataTables Api instance, with the currently selected tables for
+ * the Api's context.
+ * @param {boolean} [traditional=false] Set the API instance's context to be
+ * only the table referred to by the `DataTable.ext.iApiIndex` option, as was
+ * used in the API presented by DataTables 1.9- (i.e. the traditional mode),
+ * or if all tables captured in the jQuery object should be used.
+ * @return {DataTables.Api}
+ */
+ this.api = function ( traditional )
+ {
+ return traditional ?
+ new _Api(
+ _fnSettingsFromNode( this[ _ext.iApiIndex ] )
+ ) :
+ new _Api( this );
+ };
+
+
+ /**
+ * Add a single new row or multiple rows of data to the table. Please note
+ * that this is suitable for client-side processing only - if you are using
+ * server-side processing (i.e. "bServerSide": true), then to add data, you
+ * must add it to the data source, i.e. the server-side, through an Ajax call.
+ * @param {array|object} data The data to be added to the table. This can be:
+ *
+ * 1D array of data - add a single row with the data provided
+ * 2D array of arrays - add multiple rows in a single call
+ * object - data object when using mData
+ * array of objects - multiple data objects when using mData
+ *
+ * @param {bool} [redraw=true] redraw the table or not
+ * @returns {array} An array of integers, representing the list of indexes in
+ * aoData ({@link DataTable.models.oSettings}) that have been added to
+ * the table.
+ * @dtopt API
+ * @deprecated Since v1.10
+ *
+ * @example
+ * // Global var for counter
+ * var giCount = 2;
+ *
+ * $(document).ready(function() {
+ * $('#example').dataTable();
+ * } );
+ *
+ * function fnClickAddRow() {
+ * $('#example').dataTable().fnAddData( [
+ * giCount+".1",
+ * giCount+".2",
+ * giCount+".3",
+ * giCount+".4" ]
+ * );
+ *
+ * giCount++;
+ * }
+ */
+ this.fnAddData = function( data, redraw )
+ {
+ var api = this.api( true );
+
+ /* Check if we want to add multiple rows or not */
+ var rows = $.isArray(data) && ( $.isArray(data[0]) || $.isPlainObject(data[0]) ) ?
+ api.rows.add( data ) :
+ api.row.add( data );
+
+ if ( redraw === undefined || redraw ) {
+ api.draw();
+ }
+
+ return rows.flatten().toArray();
+ };
+
+
+ /**
+ * This function will make DataTables recalculate the column sizes, based on the data
+ * contained in the table and the sizes applied to the columns (in the DOM, CSS or
+ * through the sWidth parameter). This can be useful when the width of the table's
+ * parent element changes (for example a window resize).
+ * @param {boolean} [bRedraw=true] Redraw the table or not, you will typically want to
+ * @dtopt API
+ * @deprecated Since v1.10
+ *
+ * @example
+ * $(document).ready(function() {
+ * var oTable = $('#example').dataTable( {
+ * "sScrollY": "200px",
+ * "bPaginate": false
+ * } );
+ *
+ * $(window).on('resize', function () {
+ * oTable.fnAdjustColumnSizing();
+ * } );
+ * } );
+ */
+ this.fnAdjustColumnSizing = function ( bRedraw )
+ {
+ var api = this.api( true ).columns.adjust();
+ var settings = api.settings()[0];
+ var scroll = settings.oScroll;
+
+ if ( bRedraw === undefined || bRedraw ) {
+ api.draw( false );
+ }
+ else if ( scroll.sX !== "" || scroll.sY !== "" ) {
+ /* If not redrawing, but scrolling, we want to apply the new column sizes anyway */
+ _fnScrollDraw( settings );
+ }
+ };
+
+
+ /**
+ * Quickly and simply clear a table
+ * @param {bool} [bRedraw=true] redraw the table or not
+ * @dtopt API
+ * @deprecated Since v1.10
+ *
+ * @example
+ * $(document).ready(function() {
+ * var oTable = $('#example').dataTable();
+ *
+ * // Immediately 'nuke' the current rows (perhaps waiting for an Ajax callback...)
+ * oTable.fnClearTable();
+ * } );
+ */
+ this.fnClearTable = function( bRedraw )
+ {
+ var api = this.api( true ).clear();
+
+ if ( bRedraw === undefined || bRedraw ) {
+ api.draw();
+ }
+ };
+
+
+ /**
+ * The exact opposite of 'opening' a row, this function will close any rows which
+ * are currently 'open'.
+ * @param {node} nTr the table row to 'close'
+ * @returns {int} 0 on success, or 1 if failed (can't find the row)
+ * @dtopt API
+ * @deprecated Since v1.10
+ *
+ * @example
+ * $(document).ready(function() {
+ * var oTable;
+ *
+ * // 'open' an information row when a row is clicked on
+ * $('#example tbody tr').click( function () {
+ * if ( oTable.fnIsOpen(this) ) {
+ * oTable.fnClose( this );
+ * } else {
+ * oTable.fnOpen( this, "Temporary row opened", "info_row" );
+ * }
+ * } );
+ *
+ * oTable = $('#example').dataTable();
+ * } );
+ */
+ this.fnClose = function( nTr )
+ {
+ this.api( true ).row( nTr ).child.hide();
+ };
+
+
+ /**
+ * Remove a row for the table
+ * @param {mixed} target The index of the row from aoData to be deleted, or
+ * the TR element you want to delete
+ * @param {function|null} [callBack] Callback function
+ * @param {bool} [redraw=true] Redraw the table or not
+ * @returns {array} The row that was deleted
+ * @dtopt API
+ * @deprecated Since v1.10
+ *
+ * @example
+ * $(document).ready(function() {
+ * var oTable = $('#example').dataTable();
+ *
+ * // Immediately remove the first row
+ * oTable.fnDeleteRow( 0 );
+ * } );
+ */
+ this.fnDeleteRow = function( target, callback, redraw )
+ {
+ var api = this.api( true );
+ var rows = api.rows( target );
+ var settings = rows.settings()[0];
+ var data = settings.aoData[ rows[0][0] ];
+
+ rows.remove();
+
+ if ( callback ) {
+ callback.call( this, settings, data );
+ }
+
+ if ( redraw === undefined || redraw ) {
+ api.draw();
+ }
+
+ return data;
+ };
+
+
+ /**
+ * Restore the table to it's original state in the DOM by removing all of DataTables
+ * enhancements, alterations to the DOM structure of the table and event listeners.
+ * @param {boolean} [remove=false] Completely remove the table from the DOM
+ * @dtopt API
+ * @deprecated Since v1.10
+ *
+ * @example
+ * $(document).ready(function() {
+ * // This example is fairly pointless in reality, but shows how fnDestroy can be used
+ * var oTable = $('#example').dataTable();
+ * oTable.fnDestroy();
+ * } );
+ */
+ this.fnDestroy = function ( remove )
+ {
+ this.api( true ).destroy( remove );
+ };
+
+
+ /**
+ * Redraw the table
+ * @param {bool} [complete=true] Re-filter and resort (if enabled) the table before the draw.
+ * @dtopt API
+ * @deprecated Since v1.10
+ *
+ * @example
+ * $(document).ready(function() {
+ * var oTable = $('#example').dataTable();
+ *
+ * // Re-draw the table - you wouldn't want to do it here, but it's an example :-)
+ * oTable.fnDraw();
+ * } );
+ */
+ this.fnDraw = function( complete )
+ {
+ // Note that this isn't an exact match to the old call to _fnDraw - it takes
+ // into account the new data, but can hold position.
+ this.api( true ).draw( complete );
+ };
+
+
+ /**
+ * Filter the input based on data
+ * @param {string} sInput String to filter the table on
+ * @param {int|null} [iColumn] Column to limit filtering to
+ * @param {bool} [bRegex=false] Treat as regular expression or not
+ * @param {bool} [bSmart=true] Perform smart filtering or not
+ * @param {bool} [bShowGlobal=true] Show the input global filter in it's input box(es)
+ * @param {bool} [bCaseInsensitive=true] Do case-insensitive matching (true) or not (false)
+ * @dtopt API
+ * @deprecated Since v1.10
+ *
+ * @example
+ * $(document).ready(function() {
+ * var oTable = $('#example').dataTable();
+ *
+ * // Sometime later - filter...
+ * oTable.fnFilter( 'test string' );
+ * } );
+ */
+ this.fnFilter = function( sInput, iColumn, bRegex, bSmart, bShowGlobal, bCaseInsensitive )
+ {
+ var api = this.api( true );
+
+ if ( iColumn === null || iColumn === undefined ) {
+ api.search( sInput, bRegex, bSmart, bCaseInsensitive );
+ }
+ else {
+ api.column( iColumn ).search( sInput, bRegex, bSmart, bCaseInsensitive );
+ }
+
+ api.draw();
+ };
+
+
+ /**
+ * Get the data for the whole table, an individual row or an individual cell based on the
+ * provided parameters.
+ * @param {int|node} [src] A TR row node, TD/TH cell node or an integer. If given as
+ * a TR node then the data source for the whole row will be returned. If given as a
+ * TD/TH cell node then iCol will be automatically calculated and the data for the
+ * cell returned. If given as an integer, then this is treated as the aoData internal
+ * data index for the row (see fnGetPosition) and the data for that row used.
+ * @param {int} [col] Optional column index that you want the data of.
+ * @returns {array|object|string} If mRow is undefined, then the data for all rows is
+ * returned. If mRow is defined, just data for that row, and is iCol is
+ * defined, only data for the designated cell is returned.
+ * @dtopt API
+ * @deprecated Since v1.10
+ *
+ * @example
+ * // Row data
+ * $(document).ready(function() {
+ * oTable = $('#example').dataTable();
+ *
+ * oTable.$('tr').click( function () {
+ * var data = oTable.fnGetData( this );
+ * // ... do something with the array / object of data for the row
+ * } );
+ * } );
+ *
+ * @example
+ * // Individual cell data
+ * $(document).ready(function() {
+ * oTable = $('#example').dataTable();
+ *
+ * oTable.$('td').click( function () {
+ * var sData = oTable.fnGetData( this );
+ * alert( 'The cell clicked on had the value of '+sData );
+ * } );
+ * } );
+ */
+ this.fnGetData = function( src, col )
+ {
+ var api = this.api( true );
+
+ if ( src !== undefined ) {
+ var type = src.nodeName ? src.nodeName.toLowerCase() : '';
+
+ return col !== undefined || type == 'td' || type == 'th' ?
+ api.cell( src, col ).data() :
+ api.row( src ).data() || null;
+ }
+
+ return api.data().toArray();
+ };
+
+
+ /**
+ * Get an array of the TR nodes that are used in the table's body. Note that you will
+ * typically want to use the '$' API method in preference to this as it is more
+ * flexible.
+ * @param {int} [iRow] Optional row index for the TR element you want
+ * @returns {array|node} If iRow is undefined, returns an array of all TR elements
+ * in the table's body, or iRow is defined, just the TR element requested.
+ * @dtopt API
+ * @deprecated Since v1.10
+ *
+ * @example
+ * $(document).ready(function() {
+ * var oTable = $('#example').dataTable();
+ *
+ * // Get the nodes from the table
+ * var nNodes = oTable.fnGetNodes( );
+ * } );
+ */
+ this.fnGetNodes = function( iRow )
+ {
+ var api = this.api( true );
+
+ return iRow !== undefined ?
+ api.row( iRow ).node() :
+ api.rows().nodes().flatten().toArray();
+ };
+
+
+ /**
+ * Get the array indexes of a particular cell from it's DOM element
+ * and column index including hidden columns
+ * @param {node} node this can either be a TR, TD or TH in the table's body
+ * @returns {int} If nNode is given as a TR, then a single index is returned, or
+ * if given as a cell, an array of [row index, column index (visible),
+ * column index (all)] is given.
+ * @dtopt API
+ * @deprecated Since v1.10
+ *
+ * @example
+ * $(document).ready(function() {
+ * $('#example tbody td').click( function () {
+ * // Get the position of the current data from the node
+ * var aPos = oTable.fnGetPosition( this );
+ *
+ * // Get the data array for this row
+ * var aData = oTable.fnGetData( aPos[0] );
+ *
+ * // Update the data array and return the value
+ * aData[ aPos[1] ] = 'clicked';
+ * this.innerHTML = 'clicked';
+ * } );
+ *
+ * // Init DataTables
+ * oTable = $('#example').dataTable();
+ * } );
+ */
+ this.fnGetPosition = function( node )
+ {
+ var api = this.api( true );
+ var nodeName = node.nodeName.toUpperCase();
+
+ if ( nodeName == 'TR' ) {
+ return api.row( node ).index();
+ }
+ else if ( nodeName == 'TD' || nodeName == 'TH' ) {
+ var cell = api.cell( node ).index();
+
+ return [
+ cell.row,
+ cell.columnVisible,
+ cell.column
+ ];
+ }
+ return null;
+ };
+
+
+ /**
+ * Check to see if a row is 'open' or not.
+ * @param {node} nTr the table row to check
+ * @returns {boolean} true if the row is currently open, false otherwise
+ * @dtopt API
+ * @deprecated Since v1.10
+ *
+ * @example
+ * $(document).ready(function() {
+ * var oTable;
+ *
+ * // 'open' an information row when a row is clicked on
+ * $('#example tbody tr').click( function () {
+ * if ( oTable.fnIsOpen(this) ) {
+ * oTable.fnClose( this );
+ * } else {
+ * oTable.fnOpen( this, "Temporary row opened", "info_row" );
+ * }
+ * } );
+ *
+ * oTable = $('#example').dataTable();
+ * } );
+ */
+ this.fnIsOpen = function( nTr )
+ {
+ return this.api( true ).row( nTr ).child.isShown();
+ };
+
+
+ /**
+ * This function will place a new row directly after a row which is currently
+ * on display on the page, with the HTML contents that is passed into the
+ * function. This can be used, for example, to ask for confirmation that a
+ * particular record should be deleted.
+ * @param {node} nTr The table row to 'open'
+ * @param {string|node|jQuery} mHtml The HTML to put into the row
+ * @param {string} sClass Class to give the new TD cell
+ * @returns {node} The row opened. Note that if the table row passed in as the
+ * first parameter, is not found in the table, this method will silently
+ * return.
+ * @dtopt API
+ * @deprecated Since v1.10
+ *
+ * @example
+ * $(document).ready(function() {
+ * var oTable;
+ *
+ * // 'open' an information row when a row is clicked on
+ * $('#example tbody tr').click( function () {
+ * if ( oTable.fnIsOpen(this) ) {
+ * oTable.fnClose( this );
+ * } else {
+ * oTable.fnOpen( this, "Temporary row opened", "info_row" );
+ * }
+ * } );
+ *
+ * oTable = $('#example').dataTable();
+ * } );
+ */
+ this.fnOpen = function( nTr, mHtml, sClass )
+ {
+ return this.api( true )
+ .row( nTr )
+ .child( mHtml, sClass )
+ .show()
+ .child()[0];
+ };
+
+
+ /**
+ * Change the pagination - provides the internal logic for pagination in a simple API
+ * function. With this function you can have a DataTables table go to the next,
+ * previous, first or last pages.
+ * @param {string|int} mAction Paging action to take: "first", "previous", "next" or "last"
+ * or page number to jump to (integer), note that page 0 is the first page.
+ * @param {bool} [bRedraw=true] Redraw the table or not
+ * @dtopt API
+ * @deprecated Since v1.10
+ *
+ * @example
+ * $(document).ready(function() {
+ * var oTable = $('#example').dataTable();
+ * oTable.fnPageChange( 'next' );
+ * } );
+ */
+ this.fnPageChange = function ( mAction, bRedraw )
+ {
+ var api = this.api( true ).page( mAction );
+
+ if ( bRedraw === undefined || bRedraw ) {
+ api.draw(false);
+ }
+ };
+
+
+ /**
+ * Show a particular column
+ * @param {int} iCol The column whose display should be changed
+ * @param {bool} bShow Show (true) or hide (false) the column
+ * @param {bool} [bRedraw=true] Redraw the table or not
+ * @dtopt API
+ * @deprecated Since v1.10
+ *
+ * @example
+ * $(document).ready(function() {
+ * var oTable = $('#example').dataTable();
+ *
+ * // Hide the second column after initialisation
+ * oTable.fnSetColumnVis( 1, false );
+ * } );
+ */
+ this.fnSetColumnVis = function ( iCol, bShow, bRedraw )
+ {
+ var api = this.api( true ).column( iCol ).visible( bShow );
+
+ if ( bRedraw === undefined || bRedraw ) {
+ api.columns.adjust().draw();
+ }
+ };
+
+
+ /**
+ * Get the settings for a particular table for external manipulation
+ * @returns {object} DataTables settings object. See
+ * {@link DataTable.models.oSettings}
+ * @dtopt API
+ * @deprecated Since v1.10
+ *
+ * @example
+ * $(document).ready(function() {
+ * var oTable = $('#example').dataTable();
+ * var oSettings = oTable.fnSettings();
+ *
+ * // Show an example parameter from the settings
+ * alert( oSettings._iDisplayStart );
+ * } );
+ */
+ this.fnSettings = function()
+ {
+ return _fnSettingsFromNode( this[_ext.iApiIndex] );
+ };
+
+
+ /**
+ * Sort the table by a particular column
+ * @param {int} iCol the data index to sort on. Note that this will not match the
+ * 'display index' if you have hidden data entries
+ * @dtopt API
+ * @deprecated Since v1.10
+ *
+ * @example
+ * $(document).ready(function() {
+ * var oTable = $('#example').dataTable();
+ *
+ * // Sort immediately with columns 0 and 1
+ * oTable.fnSort( [ [0,'asc'], [1,'asc'] ] );
+ * } );
+ */
+ this.fnSort = function( aaSort )
+ {
+ this.api( true ).order( aaSort ).draw();
+ };
+
+
+ /**
+ * Attach a sort listener to an element for a given column
+ * @param {node} nNode the element to attach the sort listener to
+ * @param {int} iColumn the column that a click on this node will sort on
+ * @param {function} [fnCallback] callback function when sort is run
+ * @dtopt API
+ * @deprecated Since v1.10
+ *
+ * @example
+ * $(document).ready(function() {
+ * var oTable = $('#example').dataTable();
+ *
+ * // Sort on column 1, when 'sorter' is clicked on
+ * oTable.fnSortListener( document.getElementById('sorter'), 1 );
+ * } );
+ */
+ this.fnSortListener = function( nNode, iColumn, fnCallback )
+ {
+ this.api( true ).order.listener( nNode, iColumn, fnCallback );
+ };
+
+
+ /**
+ * Update a table cell or row - this method will accept either a single value to
+ * update the cell with, an array of values with one element for each column or
+ * an object in the same format as the original data source. The function is
+ * self-referencing in order to make the multi column updates easier.
+ * @param {object|array|string} mData Data to update the cell/row with
+ * @param {node|int} mRow TR element you want to update or the aoData index
+ * @param {int} [iColumn] The column to update, give as null or undefined to
+ * update a whole row.
+ * @param {bool} [bRedraw=true] Redraw the table or not
+ * @param {bool} [bAction=true] Perform pre-draw actions or not
+ * @returns {int} 0 on success, 1 on error
+ * @dtopt API
+ * @deprecated Since v1.10
+ *
+ * @example
+ * $(document).ready(function() {
+ * var oTable = $('#example').dataTable();
+ * oTable.fnUpdate( 'Example update', 0, 0 ); // Single cell
+ * oTable.fnUpdate( ['a', 'b', 'c', 'd', 'e'], $('tbody tr')[0] ); // Row
+ * } );
+ */
+ this.fnUpdate = function( mData, mRow, iColumn, bRedraw, bAction )
+ {
+ var api = this.api( true );
+
+ if ( iColumn === undefined || iColumn === null ) {
+ api.row( mRow ).data( mData );
+ }
+ else {
+ api.cell( mRow, iColumn ).data( mData );
+ }
+
+ if ( bAction === undefined || bAction ) {
+ api.columns.adjust();
+ }
+
+ if ( bRedraw === undefined || bRedraw ) {
+ api.draw();
+ }
+ return 0;
+ };
+
+
+ /**
+ * Provide a common method for plug-ins to check the version of DataTables being used, in order
+ * to ensure compatibility.
+ * @param {string} sVersion Version string to check for, in the format "X.Y.Z". Note that the
+ * formats "X" and "X.Y" are also acceptable.
+ * @returns {boolean} true if this version of DataTables is greater or equal to the required
+ * version, or false if this version of DataTales is not suitable
+ * @method
+ * @dtopt API
+ * @deprecated Since v1.10
+ *
+ * @example
+ * $(document).ready(function() {
+ * var oTable = $('#example').dataTable();
+ * alert( oTable.fnVersionCheck( '1.9.0' ) );
+ * } );
+ */
+ this.fnVersionCheck = _ext.fnVersionCheck;
+
+
+ var _that = this;
+ var emptyInit = options === undefined;
+ var len = this.length;
+
+ if ( emptyInit ) {
+ options = {};
+ }
+
+ this.oApi = this.internal = _ext.internal;
+
+ // Extend with old style plug-in API methods
+ for ( var fn in DataTable.ext.internal ) {
+ if ( fn ) {
+ this[fn] = _fnExternApiFunc(fn);
+ }
+ }
+
+ this.each(function() {
+ // For each initialisation we want to give it a clean initialisation
+ // object that can be bashed around
+ var o = {};
+ var oInit = len > 1 ? // optimisation for single table case
+ _fnExtend( o, options, true ) :
+ options;
+
+ /*global oInit,_that,emptyInit*/
+ var i=0, iLen, j, jLen, k, kLen;
+ var sId = this.getAttribute( 'id' );
+ var bInitHandedOff = false;
+ var defaults = DataTable.defaults;
+ var $this = $(this);
+
+
+ /* Sanity check */
+ if ( this.nodeName.toLowerCase() != 'table' )
+ {
+ _fnLog( null, 0, 'Non-table node initialisation ('+this.nodeName+')', 2 );
+ return;
+ }
+
+ /* Backwards compatibility for the defaults */
+ _fnCompatOpts( defaults );
+ _fnCompatCols( defaults.column );
+
+ /* Convert the camel-case defaults to Hungarian */
+ _fnCamelToHungarian( defaults, defaults, true );
+ _fnCamelToHungarian( defaults.column, defaults.column, true );
+
+ /* Setting up the initialisation object */
+ _fnCamelToHungarian( defaults, $.extend( oInit, $this.data() ), true );
+
+
+
+ /* Check to see if we are re-initialising a table */
+ var allSettings = DataTable.settings;
+ for ( i=0, iLen=allSettings.length ; i').appendTo($this);
+ }
+ oSettings.nTHead = thead[0];
+
+ var tbody = $this.children('tbody');
+ if ( tbody.length === 0 ) {
+ tbody = $(' ').appendTo($this);
+ }
+ oSettings.nTBody = tbody[0];
+
+ var tfoot = $this.children('tfoot');
+ if ( tfoot.length === 0 && captions.length > 0 && (oSettings.oScroll.sX !== "" || oSettings.oScroll.sY !== "") ) {
+ // If we are a scrolling table, and no footer has been given, then we need to create
+ // a tfoot element for the caption element to be appended to
+ tfoot = $(' ').appendTo($this);
+ }
+
+ if ( tfoot.length === 0 || tfoot.children().length === 0 ) {
+ $this.addClass( oClasses.sNoFooter );
+ }
+ else if ( tfoot.length > 0 ) {
+ oSettings.nTFoot = tfoot[0];
+ _fnDetectHeader( oSettings.aoFooter, oSettings.nTFoot );
+ }
+
+ /* Check if there is data passing into the constructor */
+ if ( oInit.aaData ) {
+ for ( i=0 ; i/g;
+
+ // This is not strict ISO8601 - Date.parse() is quite lax, although
+ // implementations differ between browsers.
+ var _re_date = /^\d{2,4}[\.\/\-]\d{1,2}[\.\/\-]\d{1,2}([T ]{1}\d{1,2}[:\.]\d{2}([\.:]\d{2})?)?$/;
+
+ // Escape regular expression special characters
+ var _re_escape_regex = new RegExp( '(\\' + [ '/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\', '$', '^', '-' ].join('|\\') + ')', 'g' );
+
+ // http://en.wikipedia.org/wiki/Foreign_exchange_market
+ // - \u20BD - Russian ruble.
+ // - \u20a9 - South Korean Won
+ // - \u20BA - Turkish Lira
+ // - \u20B9 - Indian Rupee
+ // - R - Brazil (R$) and South Africa
+ // - fr - Swiss Franc
+ // - kr - Swedish krona, Norwegian krone and Danish krone
+ // - \u2009 is thin space and \u202F is narrow no-break space, both used in many
+ // - Ƀ - Bitcoin
+ // - Ξ - Ethereum
+ // standards as thousands separators.
+ var _re_formatted_numeric = /[',$£€¥%\u2009\u202F\u20BD\u20a9\u20BArfkɃΞ]/gi;
+
+
+ var _empty = function ( d ) {
+ return !d || d === true || d === '-' ? true : false;
+ };
+
+
+ var _intVal = function ( s ) {
+ var integer = parseInt( s, 10 );
+ return !isNaN(integer) && isFinite(s) ? integer : null;
+ };
+
+ // Convert from a formatted number with characters other than `.` as the
+ // decimal place, to a Javascript number
+ var _numToDecimal = function ( num, decimalPoint ) {
+ // Cache created regular expressions for speed as this function is called often
+ if ( ! _re_dic[ decimalPoint ] ) {
+ _re_dic[ decimalPoint ] = new RegExp( _fnEscapeRegex( decimalPoint ), 'g' );
+ }
+ return typeof num === 'string' && decimalPoint !== '.' ?
+ num.replace( /\./g, '' ).replace( _re_dic[ decimalPoint ], '.' ) :
+ num;
+ };
+
+
+ var _isNumber = function ( d, decimalPoint, formatted ) {
+ var strType = typeof d === 'string';
+
+ // If empty return immediately so there must be a number if it is a
+ // formatted string (this stops the string "k", or "kr", etc being detected
+ // as a formatted number for currency
+ if ( _empty( d ) ) {
+ return true;
+ }
+
+ if ( decimalPoint && strType ) {
+ d = _numToDecimal( d, decimalPoint );
+ }
+
+ if ( formatted && strType ) {
+ d = d.replace( _re_formatted_numeric, '' );
+ }
+
+ return !isNaN( parseFloat(d) ) && isFinite( d );
+ };
+
+
+ // A string without HTML in it can be considered to be HTML still
+ var _isHtml = function ( d ) {
+ return _empty( d ) || typeof d === 'string';
+ };
+
+
+ var _htmlNumeric = function ( d, decimalPoint, formatted ) {
+ if ( _empty( d ) ) {
+ return true;
+ }
+
+ var html = _isHtml( d );
+ return ! html ?
+ null :
+ _isNumber( _stripHtml( d ), decimalPoint, formatted ) ?
+ true :
+ null;
+ };
+
+
+ var _pluck = function ( a, prop, prop2 ) {
+ var out = [];
+ var i=0, ien=a.length;
+
+ // Could have the test in the loop for slightly smaller code, but speed
+ // is essential here
+ if ( prop2 !== undefined ) {
+ for ( ; i')
+ .css( {
+ position: 'fixed',
+ top: 0,
+ left: $(window).scrollLeft()*-1, // allow for scrolling
+ height: 1,
+ width: 1,
+ overflow: 'hidden'
+ } )
+ .append(
+ $('
')
+ .css( {
+ position: 'absolute',
+ top: 1,
+ left: 1,
+ width: 100,
+ overflow: 'scroll'
+ } )
+ .append(
+ $('
')
+ .css( {
+ width: '100%',
+ height: 10
+ } )
+ )
+ )
+ .appendTo( 'body' );
+
+ var outer = n.children();
+ var inner = outer.children();
+
+ // Numbers below, in order, are:
+ // inner.offsetWidth, inner.clientWidth, outer.offsetWidth, outer.clientWidth
+ //
+ // IE6 XP: 100 100 100 83
+ // IE7 Vista: 100 100 100 83
+ // IE 8+ Windows: 83 83 100 83
+ // Evergreen Windows: 83 83 100 83
+ // Evergreen Mac with scrollbars: 85 85 100 85
+ // Evergreen Mac without scrollbars: 100 100 100 100
+
+ // Get scrollbar width
+ browser.barWidth = outer[0].offsetWidth - outer[0].clientWidth;
+
+ // IE6/7 will oversize a width 100% element inside a scrolling element, to
+ // include the width of the scrollbar, while other browsers ensure the inner
+ // element is contained without forcing scrolling
+ browser.bScrollOversize = inner[0].offsetWidth === 100 && outer[0].clientWidth !== 100;
+
+ // In rtl text layout, some browsers (most, but not all) will place the
+ // scrollbar on the left, rather than the right.
+ browser.bScrollbarLeft = Math.round( inner.offset().left ) !== 1;
+
+ // IE8- don't provide height and width for getBoundingClientRect
+ browser.bBounding = n[0].getBoundingClientRect().width ? true : false;
+
+ n.remove();
+ }
+
+ $.extend( settings.oBrowser, DataTable.__browser );
+ settings.oScroll.iBarWidth = DataTable.__browser.barWidth;
+ }
+
+
+ /**
+ * Array.prototype reduce[Right] method, used for browsers which don't support
+ * JS 1.6. Done this way to reduce code size, since we iterate either way
+ * @param {object} settings dataTables settings object
+ * @memberof DataTable#oApi
+ */
+ function _fnReduce ( that, fn, init, start, end, inc )
+ {
+ var
+ i = start,
+ value,
+ isSet = false;
+
+ if ( init !== undefined ) {
+ value = init;
+ isSet = true;
+ }
+
+ while ( i !== end ) {
+ if ( ! that.hasOwnProperty(i) ) {
+ continue;
+ }
+
+ value = isSet ?
+ fn( value, that[i], i, that ) :
+ that[i];
+
+ isSet = true;
+ i += inc;
+ }
+
+ return value;
+ }
+
+ /**
+ * Add a column to the list used for the table with default values
+ * @param {object} oSettings dataTables settings object
+ * @param {node} nTh The th element for this column
+ * @memberof DataTable#oApi
+ */
+ function _fnAddColumn( oSettings, nTh )
+ {
+ // Add column to aoColumns array
+ var oDefaults = DataTable.defaults.column;
+ var iCol = oSettings.aoColumns.length;
+ var oCol = $.extend( {}, DataTable.models.oColumn, oDefaults, {
+ "nTh": nTh ? nTh : document.createElement('th'),
+ "sTitle": oDefaults.sTitle ? oDefaults.sTitle : nTh ? nTh.innerHTML : '',
+ "aDataSort": oDefaults.aDataSort ? oDefaults.aDataSort : [iCol],
+ "mData": oDefaults.mData ? oDefaults.mData : iCol,
+ idx: iCol
+ } );
+ oSettings.aoColumns.push( oCol );
+
+ // Add search object for column specific search. Note that the `searchCols[ iCol ]`
+ // passed into extend can be undefined. This allows the user to give a default
+ // with only some of the parameters defined, and also not give a default
+ var searchCols = oSettings.aoPreSearchCols;
+ searchCols[ iCol ] = $.extend( {}, DataTable.models.oSearch, searchCols[ iCol ] );
+
+ // Use the default column options function to initialise classes etc
+ _fnColumnOptions( oSettings, iCol, $(nTh).data() );
+ }
+
+
+ /**
+ * Apply options for a column
+ * @param {object} oSettings dataTables settings object
+ * @param {int} iCol column index to consider
+ * @param {object} oOptions object with sType, bVisible and bSearchable etc
+ * @memberof DataTable#oApi
+ */
+ function _fnColumnOptions( oSettings, iCol, oOptions )
+ {
+ var oCol = oSettings.aoColumns[ iCol ];
+ var oClasses = oSettings.oClasses;
+ var th = $(oCol.nTh);
+
+ // Try to get width information from the DOM. We can't get it from CSS
+ // as we'd need to parse the CSS stylesheet. `width` option can override
+ if ( ! oCol.sWidthOrig ) {
+ // Width attribute
+ oCol.sWidthOrig = th.attr('width') || null;
+
+ // Style attribute
+ var t = (th.attr('style') || '').match(/width:\s*(\d+[pxem%]+)/);
+ if ( t ) {
+ oCol.sWidthOrig = t[1];
+ }
+ }
+
+ /* User specified column options */
+ if ( oOptions !== undefined && oOptions !== null )
+ {
+ // Backwards compatibility
+ _fnCompatCols( oOptions );
+
+ // Map camel case parameters to their Hungarian counterparts
+ _fnCamelToHungarian( DataTable.defaults.column, oOptions, true );
+
+ /* Backwards compatibility for mDataProp */
+ if ( oOptions.mDataProp !== undefined && !oOptions.mData )
+ {
+ oOptions.mData = oOptions.mDataProp;
+ }
+
+ if ( oOptions.sType )
+ {
+ oCol._sManualType = oOptions.sType;
+ }
+
+ // `class` is a reserved word in Javascript, so we need to provide
+ // the ability to use a valid name for the camel case input
+ if ( oOptions.className && ! oOptions.sClass )
+ {
+ oOptions.sClass = oOptions.className;
+ }
+ if ( oOptions.sClass ) {
+ th.addClass( oOptions.sClass );
+ }
+
+ $.extend( oCol, oOptions );
+ _fnMap( oCol, oOptions, "sWidth", "sWidthOrig" );
+
+ /* iDataSort to be applied (backwards compatibility), but aDataSort will take
+ * priority if defined
+ */
+ if ( oOptions.iDataSort !== undefined )
+ {
+ oCol.aDataSort = [ oOptions.iDataSort ];
+ }
+ _fnMap( oCol, oOptions, "aDataSort" );
+ }
+
+ /* Cache the data get and set functions for speed */
+ var mDataSrc = oCol.mData;
+ var mData = _fnGetObjectDataFn( mDataSrc );
+ var mRender = oCol.mRender ? _fnGetObjectDataFn( oCol.mRender ) : null;
+
+ var attrTest = function( src ) {
+ return typeof src === 'string' && src.indexOf('@') !== -1;
+ };
+ oCol._bAttrSrc = $.isPlainObject( mDataSrc ) && (
+ attrTest(mDataSrc.sort) || attrTest(mDataSrc.type) || attrTest(mDataSrc.filter)
+ );
+ oCol._setter = null;
+
+ oCol.fnGetData = function (rowData, type, meta) {
+ var innerData = mData( rowData, type, undefined, meta );
+
+ return mRender && type ?
+ mRender( innerData, type, rowData, meta ) :
+ innerData;
+ };
+ oCol.fnSetData = function ( rowData, val, meta ) {
+ return _fnSetObjectDataFn( mDataSrc )( rowData, val, meta );
+ };
+
+ // Indicate if DataTables should read DOM data as an object or array
+ // Used in _fnGetRowElements
+ if ( typeof mDataSrc !== 'number' ) {
+ oSettings._rowReadObject = true;
+ }
+
+ /* Feature sorting overrides column specific when off */
+ if ( !oSettings.oFeatures.bSort )
+ {
+ oCol.bSortable = false;
+ th.addClass( oClasses.sSortableNone ); // Have to add class here as order event isn't called
+ }
+
+ /* Check that the class assignment is correct for sorting */
+ var bAsc = $.inArray('asc', oCol.asSorting) !== -1;
+ var bDesc = $.inArray('desc', oCol.asSorting) !== -1;
+ if ( !oCol.bSortable || (!bAsc && !bDesc) )
+ {
+ oCol.sSortingClass = oClasses.sSortableNone;
+ oCol.sSortingClassJUI = "";
+ }
+ else if ( bAsc && !bDesc )
+ {
+ oCol.sSortingClass = oClasses.sSortableAsc;
+ oCol.sSortingClassJUI = oClasses.sSortJUIAscAllowed;
+ }
+ else if ( !bAsc && bDesc )
+ {
+ oCol.sSortingClass = oClasses.sSortableDesc;
+ oCol.sSortingClassJUI = oClasses.sSortJUIDescAllowed;
+ }
+ else
+ {
+ oCol.sSortingClass = oClasses.sSortable;
+ oCol.sSortingClassJUI = oClasses.sSortJUI;
+ }
+ }
+
+
+ /**
+ * Adjust the table column widths for new data. Note: you would probably want to
+ * do a redraw after calling this function!
+ * @param {object} settings dataTables settings object
+ * @memberof DataTable#oApi
+ */
+ function _fnAdjustColumnSizing ( settings )
+ {
+ /* Not interested in doing column width calculation if auto-width is disabled */
+ if ( settings.oFeatures.bAutoWidth !== false )
+ {
+ var columns = settings.aoColumns;
+
+ _fnCalculateColumnWidths( settings );
+ for ( var i=0 , iLen=columns.length ; i=0 ; i-- )
+ {
+ def = aoColDefs[i];
+
+ /* Each definition can target multiple columns, as it is an array */
+ var aTargets = def.targets !== undefined ?
+ def.targets :
+ def.aTargets;
+
+ if ( ! $.isArray( aTargets ) )
+ {
+ aTargets = [ aTargets ];
+ }
+
+ for ( j=0, jLen=aTargets.length ; j= 0 )
+ {
+ /* Add columns that we don't yet know about */
+ while( columns.length <= aTargets[j] )
+ {
+ _fnAddColumn( oSettings );
+ }
+
+ /* Integer, basic index */
+ fn( aTargets[j], def );
+ }
+ else if ( typeof aTargets[j] === 'number' && aTargets[j] < 0 )
+ {
+ /* Negative integer, right to left column counting */
+ fn( columns.length+aTargets[j], def );
+ }
+ else if ( typeof aTargets[j] === 'string' )
+ {
+ /* Class name matching on TH element */
+ for ( k=0, kLen=columns.length ; k=0 if successful (index of new aoData entry), -1 if failed
+ * @memberof DataTable#oApi
+ */
+ function _fnAddData ( oSettings, aDataIn, nTr, anTds )
+ {
+ /* Create the object for storing information about this new row */
+ var iRow = oSettings.aoData.length;
+ var oData = $.extend( true, {}, DataTable.models.oRow, {
+ src: nTr ? 'dom' : 'data',
+ idx: iRow
+ } );
+
+ oData._aData = aDataIn;
+ oSettings.aoData.push( oData );
+
+ /* Create the cells */
+ var nTd, sThisType;
+ var columns = oSettings.aoColumns;
+
+ // Invalidate the column types as the new data needs to be revalidated
+ for ( var i=0, iLen=columns.length ; i iTarget )
+ {
+ a[i]--;
+ }
+ }
+
+ if ( iTargetIndex != -1 && splice === undefined )
+ {
+ a.splice( iTargetIndex, 1 );
+ }
+ }
+
+
+ /**
+ * Mark cached data as invalid such that a re-read of the data will occur when
+ * the cached data is next requested. Also update from the data source object.
+ *
+ * @param {object} settings DataTables settings object
+ * @param {int} rowIdx Row index to invalidate
+ * @param {string} [src] Source to invalidate from: undefined, 'auto', 'dom'
+ * or 'data'
+ * @param {int} [colIdx] Column index to invalidate. If undefined the whole
+ * row will be invalidated
+ * @memberof DataTable#oApi
+ *
+ * @todo For the modularisation of v1.11 this will need to become a callback, so
+ * the sort and filter methods can subscribe to it. That will required
+ * initialisation options for sorting, which is why it is not already baked in
+ */
+ function _fnInvalidate( settings, rowIdx, src, colIdx )
+ {
+ var row = settings.aoData[ rowIdx ];
+ var i, ien;
+ var cellWrite = function ( cell, col ) {
+ // This is very frustrating, but in IE if you just write directly
+ // to innerHTML, and elements that are overwritten are GC'ed,
+ // even if there is a reference to them elsewhere
+ while ( cell.childNodes.length ) {
+ cell.removeChild( cell.firstChild );
+ }
+
+ cell.innerHTML = _fnGetCellData( settings, rowIdx, col, 'display' );
+ };
+
+ // Are we reading last data from DOM or the data object?
+ if ( src === 'dom' || ((! src || src === 'auto') && row.src === 'dom') ) {
+ // Read the data from the DOM
+ row._aData = _fnGetRowElements(
+ settings, row, colIdx, colIdx === undefined ? undefined : row._aData
+ )
+ .data;
+ }
+ else {
+ // Reading from data object, update the DOM
+ var cells = row.anCells;
+
+ if ( cells ) {
+ if ( colIdx !== undefined ) {
+ cellWrite( cells[colIdx], colIdx );
+ }
+ else {
+ for ( i=0, ien=cells.length ; i').appendTo( thead );
+ }
+
+ for ( i=0, ien=columns.length ; itr').attr('role', 'row');
+
+ /* Deal with the footer - add classes if required */
+ $(thead).find('>tr>th, >tr>td').addClass( classes.sHeaderTH );
+ $(tfoot).find('>tr>th, >tr>td').addClass( classes.sFooterTH );
+
+ // Cache the footer cells. Note that we only take the cells from the first
+ // row in the footer. If there is more than one row the user wants to
+ // interact with, they need to use the table().foot() method. Note also this
+ // allows cells to be used for multiple columns using colspan
+ if ( tfoot !== null ) {
+ var cells = oSettings.aoFooter[0];
+
+ for ( i=0, ien=cells.length ; i=0 ; j-- )
+ {
+ if ( !oSettings.aoColumns[j].bVisible && !bIncludeHidden )
+ {
+ aoLocal[i].splice( j, 1 );
+ }
+ }
+
+ /* Prep the applied array - it needs an element for each row */
+ aApplied.push( [] );
+ }
+
+ for ( i=0, iLen=aoLocal.length ; i= oSettings.fnRecordsDisplay() ?
+ 0 :
+ iInitDisplayStart;
+
+ oSettings.iInitDisplayStart = -1;
+ }
+
+ var iDisplayStart = oSettings._iDisplayStart;
+ var iDisplayEnd = oSettings.fnDisplayEnd();
+
+ /* Server-side processing draw intercept */
+ if ( oSettings.bDeferLoading )
+ {
+ oSettings.bDeferLoading = false;
+ oSettings.iDraw++;
+ _fnProcessingDisplay( oSettings, false );
+ }
+ else if ( !bServerSide )
+ {
+ oSettings.iDraw++;
+ }
+ else if ( !oSettings.bDestroying && !_fnAjaxUpdate( oSettings ) )
+ {
+ return;
+ }
+
+ if ( aiDisplay.length !== 0 )
+ {
+ var iStart = bServerSide ? 0 : iDisplayStart;
+ var iEnd = bServerSide ? oSettings.aoData.length : iDisplayEnd;
+
+ for ( var j=iStart ; j ', { 'class': iStripes ? asStripeClasses[0] : '' } )
+ .append( $(' ', {
+ 'valign': 'top',
+ 'colSpan': _fnVisbleColumns( oSettings ),
+ 'class': oSettings.oClasses.sRowEmpty
+ } ).html( sZero ) )[0];
+ }
+
+ /* Header and footer callbacks */
+ _fnCallbackFire( oSettings, 'aoHeaderCallback', 'header', [ $(oSettings.nTHead).children('tr')[0],
+ _fnGetDataMaster( oSettings ), iDisplayStart, iDisplayEnd, aiDisplay ] );
+
+ _fnCallbackFire( oSettings, 'aoFooterCallback', 'footer', [ $(oSettings.nTFoot).children('tr')[0],
+ _fnGetDataMaster( oSettings ), iDisplayStart, iDisplayEnd, aiDisplay ] );
+
+ var body = $(oSettings.nTBody);
+
+ body.children().detach();
+ body.append( $(anRows) );
+
+ /* Call all required callback functions for the end of a draw */
+ _fnCallbackFire( oSettings, 'aoDrawCallback', 'draw', [oSettings] );
+
+ /* Draw is complete, sorting and filtering must be as well */
+ oSettings.bSorted = false;
+ oSettings.bFiltered = false;
+ oSettings.bDrawing = false;
+ }
+
+
+ /**
+ * Redraw the table - taking account of the various features which are enabled
+ * @param {object} oSettings dataTables settings object
+ * @param {boolean} [holdPosition] Keep the current paging position. By default
+ * the paging is reset to the first page
+ * @memberof DataTable#oApi
+ */
+ function _fnReDraw( settings, holdPosition )
+ {
+ var
+ features = settings.oFeatures,
+ sort = features.bSort,
+ filter = features.bFilter;
+
+ if ( sort ) {
+ _fnSort( settings );
+ }
+
+ if ( filter ) {
+ _fnFilterComplete( settings, settings.oPreviousSearch );
+ }
+ else {
+ // No filtering, so we want to just use the display master
+ settings.aiDisplay = settings.aiDisplayMaster.slice();
+ }
+
+ if ( holdPosition !== true ) {
+ settings._iDisplayStart = 0;
+ }
+
+ // Let any modules know about the draw hold position state (used by
+ // scrolling internally)
+ settings._drawHold = holdPosition;
+
+ _fnDraw( settings );
+
+ settings._drawHold = false;
+ }
+
+
+ /**
+ * Add the options to the page HTML for the table
+ * @param {object} oSettings dataTables settings object
+ * @memberof DataTable#oApi
+ */
+ function _fnAddOptionsHtml ( oSettings )
+ {
+ var classes = oSettings.oClasses;
+ var table = $(oSettings.nTable);
+ var holding = $('
').insertBefore( table ); // Holding element for speed
+ var features = oSettings.oFeatures;
+
+ // All DataTables are wrapped in a div
+ var insert = $('
', {
+ id: oSettings.sTableId+'_wrapper',
+ 'class': classes.sWrapper + (oSettings.nTFoot ? '' : ' '+classes.sNoFooter)
+ } );
+
+ oSettings.nHolding = holding[0];
+ oSettings.nTableWrapper = insert[0];
+ oSettings.nTableReinsertBefore = oSettings.nTable.nextSibling;
+
+ /* Loop over the user set positioning and place the elements as needed */
+ var aDom = oSettings.sDom.split('');
+ var featureNode, cOption, nNewNode, cNext, sAttr, j;
+ for ( var i=0 ; i')[0];
+
+ /* Check to see if we should append an id and/or a class name to the container */
+ cNext = aDom[i+1];
+ if ( cNext == "'" || cNext == '"' )
+ {
+ sAttr = "";
+ j = 2;
+ while ( aDom[i+j] != cNext )
+ {
+ sAttr += aDom[i+j];
+ j++;
+ }
+
+ /* Replace jQuery UI constants @todo depreciated */
+ if ( sAttr == "H" )
+ {
+ sAttr = classes.sJUIHeader;
+ }
+ else if ( sAttr == "F" )
+ {
+ sAttr = classes.sJUIFooter;
+ }
+
+ /* The attribute can be in the format of "#id.class", "#id" or "class" This logic
+ * breaks the string into parts and applies them as needed
+ */
+ if ( sAttr.indexOf('.') != -1 )
+ {
+ var aSplit = sAttr.split('.');
+ nNewNode.id = aSplit[0].substr(1, aSplit[0].length-1);
+ nNewNode.className = aSplit[1];
+ }
+ else if ( sAttr.charAt(0) == "#" )
+ {
+ nNewNode.id = sAttr.substr(1, sAttr.length-1);
+ }
+ else
+ {
+ nNewNode.className = sAttr;
+ }
+
+ i += j; /* Move along the position array */
+ }
+
+ insert.append( nNewNode );
+ insert = $(nNewNode);
+ }
+ else if ( cOption == '>' )
+ {
+ /* End container div */
+ insert = insert.parent();
+ }
+ // @todo Move options into their own plugins?
+ else if ( cOption == 'l' && features.bPaginate && features.bLengthChange )
+ {
+ /* Length */
+ featureNode = _fnFeatureHtmlLength( oSettings );
+ }
+ else if ( cOption == 'f' && features.bFilter )
+ {
+ /* Filter */
+ featureNode = _fnFeatureHtmlFilter( oSettings );
+ }
+ else if ( cOption == 'r' && features.bProcessing )
+ {
+ /* pRocessing */
+ featureNode = _fnFeatureHtmlProcessing( oSettings );
+ }
+ else if ( cOption == 't' )
+ {
+ /* Table */
+ featureNode = _fnFeatureHtmlTable( oSettings );
+ }
+ else if ( cOption == 'i' && features.bInfo )
+ {
+ /* Info */
+ featureNode = _fnFeatureHtmlInfo( oSettings );
+ }
+ else if ( cOption == 'p' && features.bPaginate )
+ {
+ /* Pagination */
+ featureNode = _fnFeatureHtmlPaginate( oSettings );
+ }
+ else if ( DataTable.ext.feature.length !== 0 )
+ {
+ /* Plug-in features */
+ var aoFeatures = DataTable.ext.feature;
+ for ( var k=0, kLen=aoFeatures.length ; k ';
+
+ var str = language.sSearch;
+ str = str.match(/_INPUT_/) ?
+ str.replace('_INPUT_', input) :
+ str+input;
+
+ var filter = $('
', {
+ 'id': ! features.f ? tableId+'_filter' : null,
+ 'class': classes.sFilter
+ } )
+ .append( $(' ' ).append( str ) );
+
+ var searchFn = function() {
+ /* Update all other filter input elements for the new display */
+ var n = features.f;
+ var val = !this.value ? "" : this.value; // mental IE8 fix :-(
+
+ /* Now do the filter */
+ if ( val != previousSearch.sSearch ) {
+ _fnFilterComplete( settings, {
+ "sSearch": val,
+ "bRegex": previousSearch.bRegex,
+ "bSmart": previousSearch.bSmart ,
+ "bCaseInsensitive": previousSearch.bCaseInsensitive
+ } );
+
+ // Need to redraw, without resorting
+ settings._iDisplayStart = 0;
+ _fnDraw( settings );
+ }
+ };
+
+ var searchDelay = settings.searchDelay !== null ?
+ settings.searchDelay :
+ _fnDataSource( settings ) === 'ssp' ?
+ 400 :
+ 0;
+
+ var jqFilter = $('input', filter)
+ .val( previousSearch.sSearch )
+ .attr( 'placeholder', language.sSearchPlaceholder )
+ .on(
+ 'keyup.DT search.DT input.DT paste.DT cut.DT',
+ searchDelay ?
+ _fnThrottle( searchFn, searchDelay ) :
+ searchFn
+ )
+ .on( 'keypress.DT', function(e) {
+ /* Prevent form submission */
+ if ( e.keyCode == 13 ) {
+ return false;
+ }
+ } )
+ .attr('aria-controls', tableId);
+
+ // Update the input elements whenever the table is filtered
+ $(settings.nTable).on( 'search.dt.DT', function ( ev, s ) {
+ if ( settings === s ) {
+ // IE9 throws an 'unknown error' if document.activeElement is used
+ // inside an iframe or frame...
+ try {
+ if ( jqFilter[0] !== document.activeElement ) {
+ jqFilter.val( previousSearch.sSearch );
+ }
+ }
+ catch ( e ) {}
+ }
+ } );
+
+ return filter[0];
+ }
+
+
+ /**
+ * Filter the table using both the global filter and column based filtering
+ * @param {object} oSettings dataTables settings object
+ * @param {object} oSearch search information
+ * @param {int} [iForce] force a research of the master array (1) or not (undefined or 0)
+ * @memberof DataTable#oApi
+ */
+ function _fnFilterComplete ( oSettings, oInput, iForce )
+ {
+ var oPrevSearch = oSettings.oPreviousSearch;
+ var aoPrevSearch = oSettings.aoPreSearchCols;
+ var fnSaveFilter = function ( oFilter ) {
+ /* Save the filtering values */
+ oPrevSearch.sSearch = oFilter.sSearch;
+ oPrevSearch.bRegex = oFilter.bRegex;
+ oPrevSearch.bSmart = oFilter.bSmart;
+ oPrevSearch.bCaseInsensitive = oFilter.bCaseInsensitive;
+ };
+ var fnRegex = function ( o ) {
+ // Backwards compatibility with the bEscapeRegex option
+ return o.bEscapeRegex !== undefined ? !o.bEscapeRegex : o.bRegex;
+ };
+
+ // Resolve any column types that are unknown due to addition or invalidation
+ // @todo As per sort - can this be moved into an event handler?
+ _fnColumnTypes( oSettings );
+
+ /* In server-side processing all filtering is done by the server, so no point hanging around here */
+ if ( _fnDataSource( oSettings ) != 'ssp' )
+ {
+ /* Global filter */
+ _fnFilter( oSettings, oInput.sSearch, iForce, fnRegex(oInput), oInput.bSmart, oInput.bCaseInsensitive );
+ fnSaveFilter( oInput );
+
+ /* Now do the individual column filter */
+ for ( var i=0 ; i input.length ||
+ input.indexOf(prevSearch) !== 0 ||
+ settings.bSorted // On resort, the display master needs to be
+ // re-filtered since indexes will have changed
+ ) {
+ settings.aiDisplay = displayMaster.slice();
+ }
+
+ // Search the display array
+ display = settings.aiDisplay;
+
+ for ( i=0 ; i')[0];
+ var __filter_div_textContent = __filter_div.textContent !== undefined;
+
+ // Update the filtering data for each row if needed (by invalidation or first run)
+ function _fnFilterData ( settings )
+ {
+ var columns = settings.aoColumns;
+ var column;
+ var i, j, ien, jen, filterData, cellData, row;
+ var fomatters = DataTable.ext.type.search;
+ var wasInvalidated = false;
+
+ for ( i=0, ien=settings.aoData.length ; i', {
+ 'class': settings.oClasses.sInfo,
+ 'id': ! nodes ? tid+'_info' : null
+ } );
+
+ if ( ! nodes ) {
+ // Update display on each draw
+ settings.aoDrawCallback.push( {
+ "fn": _fnUpdateInfo,
+ "sName": "information"
+ } );
+
+ n
+ .attr( 'role', 'status' )
+ .attr( 'aria-live', 'polite' );
+
+ // Table is described by our info div
+ $(settings.nTable).attr( 'aria-describedby', tid+'_info' );
+ }
+
+ return n[0];
+ }
+
+
+ /**
+ * Update the information elements in the display
+ * @param {object} settings dataTables settings object
+ * @memberof DataTable#oApi
+ */
+ function _fnUpdateInfo ( settings )
+ {
+ /* Show information about the table */
+ var nodes = settings.aanFeatures.i;
+ if ( nodes.length === 0 ) {
+ return;
+ }
+
+ var
+ lang = settings.oLanguage,
+ start = settings._iDisplayStart+1,
+ end = settings.fnDisplayEnd(),
+ max = settings.fnRecordsTotal(),
+ total = settings.fnRecordsDisplay(),
+ out = total ?
+ lang.sInfo :
+ lang.sInfoEmpty;
+
+ if ( total !== max ) {
+ /* Record set after filtering */
+ out += ' ' + lang.sInfoFiltered;
+ }
+
+ // Convert the macros
+ out += lang.sInfoPostFix;
+ out = _fnInfoMacros( settings, out );
+
+ var callback = lang.fnInfoCallback;
+ if ( callback !== null ) {
+ out = callback.call( settings.oInstance,
+ settings, start, end, max, total, out
+ );
+ }
+
+ $(nodes).html( out );
+ }
+
+
+ function _fnInfoMacros ( settings, str )
+ {
+ // When infinite scrolling, we are always starting at 1. _iDisplayStart is used only
+ // internally
+ var
+ formatter = settings.fnFormatNumber,
+ start = settings._iDisplayStart+1,
+ len = settings._iDisplayLength,
+ vis = settings.fnRecordsDisplay(),
+ all = len === -1;
+
+ return str.
+ replace(/_START_/g, formatter.call( settings, start ) ).
+ replace(/_END_/g, formatter.call( settings, settings.fnDisplayEnd() ) ).
+ replace(/_MAX_/g, formatter.call( settings, settings.fnRecordsTotal() ) ).
+ replace(/_TOTAL_/g, formatter.call( settings, vis ) ).
+ replace(/_PAGE_/g, formatter.call( settings, all ? 1 : Math.ceil( start / len ) ) ).
+ replace(/_PAGES_/g, formatter.call( settings, all ? 1 : Math.ceil( vis / len ) ) );
+ }
+
+
+
+ /**
+ * Draw the table for the first time, adding all required features
+ * @param {object} settings dataTables settings object
+ * @memberof DataTable#oApi
+ */
+ function _fnInitialise ( settings )
+ {
+ var i, iLen, iAjaxStart=settings.iInitDisplayStart;
+ var columns = settings.aoColumns, column;
+ var features = settings.oFeatures;
+ var deferLoading = settings.bDeferLoading; // value modified by the draw
+
+ /* Ensure that the table data is fully initialised */
+ if ( ! settings.bInitialised ) {
+ setTimeout( function(){ _fnInitialise( settings ); }, 200 );
+ return;
+ }
+
+ /* Show the display HTML options */
+ _fnAddOptionsHtml( settings );
+
+ /* Build and draw the header / footer for the table */
+ _fnBuildHead( settings );
+ _fnDrawHead( settings, settings.aoHeader );
+ _fnDrawHead( settings, settings.aoFooter );
+
+ /* Okay to show that something is going on now */
+ _fnProcessingDisplay( settings, true );
+
+ /* Calculate sizes for columns */
+ if ( features.bAutoWidth ) {
+ _fnCalculateColumnWidths( settings );
+ }
+
+ for ( i=0, iLen=columns.length ; i', {
+ 'name': tableId+'_length',
+ 'aria-controls': tableId,
+ 'class': classes.sLengthSelect
+ } );
+
+ for ( var i=0, ien=lengths.length ; i ').addClass( classes.sLength );
+ if ( ! settings.aanFeatures.l ) {
+ div[0].id = tableId+'_length';
+ }
+
+ div.children().append(
+ settings.oLanguage.sLengthMenu.replace( '_MENU_', select[0].outerHTML )
+ );
+
+ // Can't use `select` variable as user might provide their own and the
+ // reference is broken by the use of outerHTML
+ $('select', div)
+ .val( settings._iDisplayLength )
+ .on( 'change.DT', function(e) {
+ _fnLengthChange( settings, $(this).val() );
+ _fnDraw( settings );
+ } );
+
+ // Update node value whenever anything changes the table's length
+ $(settings.nTable).on( 'length.dt.DT', function (e, s, len) {
+ if ( settings === s ) {
+ $('select', div).val( len );
+ }
+ } );
+
+ return div[0];
+ }
+
+
+
+ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * Note that most of the paging logic is done in
+ * DataTable.ext.pager
+ */
+
+ /**
+ * Generate the node required for default pagination
+ * @param {object} oSettings dataTables settings object
+ * @returns {node} Pagination feature node
+ * @memberof DataTable#oApi
+ */
+ function _fnFeatureHtmlPaginate ( settings )
+ {
+ var
+ type = settings.sPaginationType,
+ plugin = DataTable.ext.pager[ type ],
+ modern = typeof plugin === 'function',
+ redraw = function( settings ) {
+ _fnDraw( settings );
+ },
+ node = $('
').addClass( settings.oClasses.sPaging + type )[0],
+ features = settings.aanFeatures;
+
+ if ( ! modern ) {
+ plugin.fnInit( settings, node, redraw );
+ }
+
+ /* Add a draw callback for the pagination on first instance, to update the paging display */
+ if ( ! features.p )
+ {
+ node.id = settings.sTableId+'_paginate';
+
+ settings.aoDrawCallback.push( {
+ "fn": function( settings ) {
+ if ( modern ) {
+ var
+ start = settings._iDisplayStart,
+ len = settings._iDisplayLength,
+ visRecords = settings.fnRecordsDisplay(),
+ all = len === -1,
+ page = all ? 0 : Math.ceil( start / len ),
+ pages = all ? 1 : Math.ceil( visRecords / len ),
+ buttons = plugin(page, pages),
+ i, ien;
+
+ for ( i=0, ien=features.p.length ; i records )
+ {
+ start = 0;
+ }
+ }
+ else if ( action == "first" )
+ {
+ start = 0;
+ }
+ else if ( action == "previous" )
+ {
+ start = len >= 0 ?
+ start - len :
+ 0;
+
+ if ( start < 0 )
+ {
+ start = 0;
+ }
+ }
+ else if ( action == "next" )
+ {
+ if ( start + len < records )
+ {
+ start += len;
+ }
+ }
+ else if ( action == "last" )
+ {
+ start = Math.floor( (records-1) / len) * len;
+ }
+ else
+ {
+ _fnLog( settings, 0, "Unknown paging action: "+action, 5 );
+ }
+
+ var changed = settings._iDisplayStart !== start;
+ settings._iDisplayStart = start;
+
+ if ( changed ) {
+ _fnCallbackFire( settings, null, 'page', [settings] );
+
+ if ( redraw ) {
+ _fnDraw( settings );
+ }
+ }
+
+ return changed;
+ }
+
+
+
+ /**
+ * Generate the node required for the processing node
+ * @param {object} settings dataTables settings object
+ * @returns {node} Processing element
+ * @memberof DataTable#oApi
+ */
+ function _fnFeatureHtmlProcessing ( settings )
+ {
+ return $('
', {
+ 'id': ! settings.aanFeatures.r ? settings.sTableId+'_processing' : null,
+ 'class': settings.oClasses.sProcessing
+ } )
+ .html( settings.oLanguage.sProcessing )
+ .insertBefore( settings.nTable )[0];
+ }
+
+
+ /**
+ * Display or hide the processing indicator
+ * @param {object} settings dataTables settings object
+ * @param {bool} show Show the processing indicator (true) or not (false)
+ * @memberof DataTable#oApi
+ */
+ function _fnProcessingDisplay ( settings, show )
+ {
+ if ( settings.oFeatures.bProcessing ) {
+ $(settings.aanFeatures.r).css( 'display', show ? 'block' : 'none' );
+ }
+
+ _fnCallbackFire( settings, null, 'processing', [settings, show] );
+ }
+
+ /**
+ * Add any control elements for the table - specifically scrolling
+ * @param {object} settings dataTables settings object
+ * @returns {node} Node to add to the DOM
+ * @memberof DataTable#oApi
+ */
+ function _fnFeatureHtmlTable ( settings )
+ {
+ var table = $(settings.nTable);
+
+ // Add the ARIA grid role to the table
+ table.attr( 'role', 'grid' );
+
+ // Scrolling from here on in
+ var scroll = settings.oScroll;
+
+ if ( scroll.sX === '' && scroll.sY === '' ) {
+ return settings.nTable;
+ }
+
+ var scrollX = scroll.sX;
+ var scrollY = scroll.sY;
+ var classes = settings.oClasses;
+ var caption = table.children('caption');
+ var captionSide = caption.length ? caption[0]._captionSide : null;
+ var headerClone = $( table[0].cloneNode(false) );
+ var footerClone = $( table[0].cloneNode(false) );
+ var footer = table.children('tfoot');
+ var _div = '
';
+ var size = function ( s ) {
+ return !s ? null : _fnStringToCss( s );
+ };
+
+ if ( ! footer.length ) {
+ footer = null;
+ }
+
+ /*
+ * The HTML structure that we want to generate in this function is:
+ * div - scroller
+ * div - scroll head
+ * div - scroll head inner
+ * table - scroll head table
+ * thead - thead
+ * div - scroll body
+ * table - table (master table)
+ * thead - thead clone for sizing
+ * tbody - tbody
+ * div - scroll foot
+ * div - scroll foot inner
+ * table - scroll foot table
+ * tfoot - tfoot
+ */
+ var scroller = $( _div, { 'class': classes.sScrollWrapper } )
+ .append(
+ $(_div, { 'class': classes.sScrollHead } )
+ .css( {
+ overflow: 'hidden',
+ position: 'relative',
+ border: 0,
+ width: scrollX ? size(scrollX) : '100%'
+ } )
+ .append(
+ $(_div, { 'class': classes.sScrollHeadInner } )
+ .css( {
+ 'box-sizing': 'content-box',
+ width: scroll.sXInner || '100%'
+ } )
+ .append(
+ headerClone
+ .removeAttr('id')
+ .css( 'margin-left', 0 )
+ .append( captionSide === 'top' ? caption : null )
+ .append(
+ table.children('thead')
+ )
+ )
+ )
+ )
+ .append(
+ $(_div, { 'class': classes.sScrollBody } )
+ .css( {
+ position: 'relative',
+ overflow: 'auto',
+ width: size( scrollX )
+ } )
+ .append( table )
+ );
+
+ if ( footer ) {
+ scroller.append(
+ $(_div, { 'class': classes.sScrollFoot } )
+ .css( {
+ overflow: 'hidden',
+ border: 0,
+ width: scrollX ? size(scrollX) : '100%'
+ } )
+ .append(
+ $(_div, { 'class': classes.sScrollFootInner } )
+ .append(
+ footerClone
+ .removeAttr('id')
+ .css( 'margin-left', 0 )
+ .append( captionSide === 'bottom' ? caption : null )
+ .append(
+ table.children('tfoot')
+ )
+ )
+ )
+ );
+ }
+
+ var children = scroller.children();
+ var scrollHead = children[0];
+ var scrollBody = children[1];
+ var scrollFoot = footer ? children[2] : null;
+
+ // When the body is scrolled, then we also want to scroll the headers
+ if ( scrollX ) {
+ $(scrollBody).on( 'scroll.DT', function (e) {
+ var scrollLeft = this.scrollLeft;
+
+ scrollHead.scrollLeft = scrollLeft;
+
+ if ( footer ) {
+ scrollFoot.scrollLeft = scrollLeft;
+ }
+ } );
+ }
+
+ $(scrollBody).css(
+ scrollY && scroll.bCollapse ? 'max-height' : 'height',
+ scrollY
+ );
+
+ settings.nScrollHead = scrollHead;
+ settings.nScrollBody = scrollBody;
+ settings.nScrollFoot = scrollFoot;
+
+ // On redraw - align columns
+ settings.aoDrawCallback.push( {
+ "fn": _fnScrollDraw,
+ "sName": "scrolling"
+ } );
+
+ return scroller[0];
+ }
+
+
+
+ /**
+ * Update the header, footer and body tables for resizing - i.e. column
+ * alignment.
+ *
+ * Welcome to the most horrible function DataTables. The process that this
+ * function follows is basically:
+ * 1. Re-create the table inside the scrolling div
+ * 2. Take live measurements from the DOM
+ * 3. Apply the measurements to align the columns
+ * 4. Clean up
+ *
+ * @param {object} settings dataTables settings object
+ * @memberof DataTable#oApi
+ */
+ function _fnScrollDraw ( settings )
+ {
+ // Given that this is such a monster function, a lot of variables are use
+ // to try and keep the minimised size as small as possible
+ var
+ scroll = settings.oScroll,
+ scrollX = scroll.sX,
+ scrollXInner = scroll.sXInner,
+ scrollY = scroll.sY,
+ barWidth = scroll.iBarWidth,
+ divHeader = $(settings.nScrollHead),
+ divHeaderStyle = divHeader[0].style,
+ divHeaderInner = divHeader.children('div'),
+ divHeaderInnerStyle = divHeaderInner[0].style,
+ divHeaderTable = divHeaderInner.children('table'),
+ divBodyEl = settings.nScrollBody,
+ divBody = $(divBodyEl),
+ divBodyStyle = divBodyEl.style,
+ divFooter = $(settings.nScrollFoot),
+ divFooterInner = divFooter.children('div'),
+ divFooterTable = divFooterInner.children('table'),
+ header = $(settings.nTHead),
+ table = $(settings.nTable),
+ tableEl = table[0],
+ tableStyle = tableEl.style,
+ footer = settings.nTFoot ? $(settings.nTFoot) : null,
+ browser = settings.oBrowser,
+ ie67 = browser.bScrollOversize,
+ dtHeaderCells = _pluck( settings.aoColumns, 'nTh' ),
+ headerTrgEls, footerTrgEls,
+ headerSrcEls, footerSrcEls,
+ headerCopy, footerCopy,
+ headerWidths=[], footerWidths=[],
+ headerContent=[], footerContent=[],
+ idx, correction, sanityWidth,
+ zeroOut = function(nSizer) {
+ var style = nSizer.style;
+ style.paddingTop = "0";
+ style.paddingBottom = "0";
+ style.borderTopWidth = "0";
+ style.borderBottomWidth = "0";
+ style.height = 0;
+ };
+
+ // If the scrollbar visibility has changed from the last draw, we need to
+ // adjust the column sizes as the table width will have changed to account
+ // for the scrollbar
+ var scrollBarVis = divBodyEl.scrollHeight > divBodyEl.clientHeight;
+
+ if ( settings.scrollBarVis !== scrollBarVis && settings.scrollBarVis !== undefined ) {
+ settings.scrollBarVis = scrollBarVis;
+ _fnAdjustColumnSizing( settings );
+ return; // adjust column sizing will call this function again
+ }
+ else {
+ settings.scrollBarVis = scrollBarVis;
+ }
+
+ /*
+ * 1. Re-create the table inside the scrolling div
+ */
+
+ // Remove the old minimised thead and tfoot elements in the inner table
+ table.children('thead, tfoot').remove();
+
+ if ( footer ) {
+ footerCopy = footer.clone().prependTo( table );
+ footerTrgEls = footer.find('tr'); // the original tfoot is in its own table and must be sized
+ footerSrcEls = footerCopy.find('tr');
+ }
+
+ // Clone the current header and footer elements and then place it into the inner table
+ headerCopy = header.clone().prependTo( table );
+ headerTrgEls = header.find('tr'); // original header is in its own table
+ headerSrcEls = headerCopy.find('tr');
+ headerCopy.find('th, td').removeAttr('tabindex');
+
+
+ /*
+ * 2. Take live measurements from the DOM - do not alter the DOM itself!
+ */
+
+ // Remove old sizing and apply the calculated column widths
+ // Get the unique column headers in the newly created (cloned) header. We want to apply the
+ // calculated sizes to this header
+ if ( ! scrollX )
+ {
+ divBodyStyle.width = '100%';
+ divHeader[0].style.width = '100%';
+ }
+
+ $.each( _fnGetUniqueThs( settings, headerCopy ), function ( i, el ) {
+ idx = _fnVisibleToColumnIndex( settings, i );
+ el.style.width = settings.aoColumns[idx].sWidth;
+ } );
+
+ if ( footer ) {
+ _fnApplyToChildren( function(n) {
+ n.style.width = "";
+ }, footerSrcEls );
+ }
+
+ // Size the table as a whole
+ sanityWidth = table.outerWidth();
+ if ( scrollX === "" ) {
+ // No x scrolling
+ tableStyle.width = "100%";
+
+ // IE7 will make the width of the table when 100% include the scrollbar
+ // - which is shouldn't. When there is a scrollbar we need to take this
+ // into account.
+ if ( ie67 && (table.find('tbody').height() > divBodyEl.offsetHeight ||
+ divBody.css('overflow-y') == "scroll")
+ ) {
+ tableStyle.width = _fnStringToCss( table.outerWidth() - barWidth);
+ }
+
+ // Recalculate the sanity width
+ sanityWidth = table.outerWidth();
+ }
+ else if ( scrollXInner !== "" ) {
+ // legacy x scroll inner has been given - use it
+ tableStyle.width = _fnStringToCss(scrollXInner);
+
+ // Recalculate the sanity width
+ sanityWidth = table.outerWidth();
+ }
+
+ // Hidden header should have zero height, so remove padding and borders. Then
+ // set the width based on the real headers
+
+ // Apply all styles in one pass
+ _fnApplyToChildren( zeroOut, headerSrcEls );
+
+ // Read all widths in next pass
+ _fnApplyToChildren( function(nSizer) {
+ headerContent.push( nSizer.innerHTML );
+ headerWidths.push( _fnStringToCss( $(nSizer).css('width') ) );
+ }, headerSrcEls );
+
+ // Apply all widths in final pass
+ _fnApplyToChildren( function(nToSize, i) {
+ // Only apply widths to the DataTables detected header cells - this
+ // prevents complex headers from having contradictory sizes applied
+ if ( $.inArray( nToSize, dtHeaderCells ) !== -1 ) {
+ nToSize.style.width = headerWidths[i];
+ }
+ }, headerTrgEls );
+
+ $(headerSrcEls).height(0);
+
+ /* Same again with the footer if we have one */
+ if ( footer )
+ {
+ _fnApplyToChildren( zeroOut, footerSrcEls );
+
+ _fnApplyToChildren( function(nSizer) {
+ footerContent.push( nSizer.innerHTML );
+ footerWidths.push( _fnStringToCss( $(nSizer).css('width') ) );
+ }, footerSrcEls );
+
+ _fnApplyToChildren( function(nToSize, i) {
+ nToSize.style.width = footerWidths[i];
+ }, footerTrgEls );
+
+ $(footerSrcEls).height(0);
+ }
+
+
+ /*
+ * 3. Apply the measurements
+ */
+
+ // "Hide" the header and footer that we used for the sizing. We need to keep
+ // the content of the cell so that the width applied to the header and body
+ // both match, but we want to hide it completely. We want to also fix their
+ // width to what they currently are
+ _fnApplyToChildren( function(nSizer, i) {
+ nSizer.innerHTML = ''+headerContent[i]+'
';
+ nSizer.childNodes[0].style.height = "0";
+ nSizer.childNodes[0].style.overflow = "hidden";
+ nSizer.style.width = headerWidths[i];
+ }, headerSrcEls );
+
+ if ( footer )
+ {
+ _fnApplyToChildren( function(nSizer, i) {
+ nSizer.innerHTML = ''+footerContent[i]+'
';
+ nSizer.childNodes[0].style.height = "0";
+ nSizer.childNodes[0].style.overflow = "hidden";
+ nSizer.style.width = footerWidths[i];
+ }, footerSrcEls );
+ }
+
+ // Sanity check that the table is of a sensible width. If not then we are going to get
+ // misalignment - try to prevent this by not allowing the table to shrink below its min width
+ if ( table.outerWidth() < sanityWidth )
+ {
+ // The min width depends upon if we have a vertical scrollbar visible or not */
+ correction = ((divBodyEl.scrollHeight > divBodyEl.offsetHeight ||
+ divBody.css('overflow-y') == "scroll")) ?
+ sanityWidth+barWidth :
+ sanityWidth;
+
+ // IE6/7 are a law unto themselves...
+ if ( ie67 && (divBodyEl.scrollHeight >
+ divBodyEl.offsetHeight || divBody.css('overflow-y') == "scroll")
+ ) {
+ tableStyle.width = _fnStringToCss( correction-barWidth );
+ }
+
+ // And give the user a warning that we've stopped the table getting too small
+ if ( scrollX === "" || scrollXInner !== "" ) {
+ _fnLog( settings, 1, 'Possible column misalignment', 6 );
+ }
+ }
+ else
+ {
+ correction = '100%';
+ }
+
+ // Apply to the container elements
+ divBodyStyle.width = _fnStringToCss( correction );
+ divHeaderStyle.width = _fnStringToCss( correction );
+
+ if ( footer ) {
+ settings.nScrollFoot.style.width = _fnStringToCss( correction );
+ }
+
+
+ /*
+ * 4. Clean up
+ */
+ if ( ! scrollY ) {
+ /* IE7< puts a vertical scrollbar in place (when it shouldn't be) due to subtracting
+ * the scrollbar height from the visible display, rather than adding it on. We need to
+ * set the height in order to sort this. Don't want to do it in any other browsers.
+ */
+ if ( ie67 ) {
+ divBodyStyle.height = _fnStringToCss( tableEl.offsetHeight+barWidth );
+ }
+ }
+
+ /* Finally set the width's of the header and footer tables */
+ var iOuterWidth = table.outerWidth();
+ divHeaderTable[0].style.width = _fnStringToCss( iOuterWidth );
+ divHeaderInnerStyle.width = _fnStringToCss( iOuterWidth );
+
+ // Figure out if there are scrollbar present - if so then we need a the header and footer to
+ // provide a bit more space to allow "overflow" scrolling (i.e. past the scrollbar)
+ var bScrolling = table.height() > divBodyEl.clientHeight || divBody.css('overflow-y') == "scroll";
+ var padding = 'padding' + (browser.bScrollbarLeft ? 'Left' : 'Right' );
+ divHeaderInnerStyle[ padding ] = bScrolling ? barWidth+"px" : "0px";
+
+ if ( footer ) {
+ divFooterTable[0].style.width = _fnStringToCss( iOuterWidth );
+ divFooterInner[0].style.width = _fnStringToCss( iOuterWidth );
+ divFooterInner[0].style[padding] = bScrolling ? barWidth+"px" : "0px";
+ }
+
+ // Correct DOM ordering for colgroup - comes before the thead
+ table.children('colgroup').insertBefore( table.children('thead') );
+
+ /* Adjust the position of the header in case we loose the y-scrollbar */
+ divBody.trigger('scroll');
+
+ // If sorting or filtering has occurred, jump the scrolling back to the top
+ // only if we aren't holding the position
+ if ( (settings.bSorted || settings.bFiltered) && ! settings._drawHold ) {
+ divBodyEl.scrollTop = 0;
+ }
+ }
+
+
+
+ /**
+ * Apply a given function to the display child nodes of an element array (typically
+ * TD children of TR rows
+ * @param {function} fn Method to apply to the objects
+ * @param array {nodes} an1 List of elements to look through for display children
+ * @param array {nodes} an2 Another list (identical structure to the first) - optional
+ * @memberof DataTable#oApi
+ */
+ function _fnApplyToChildren( fn, an1, an2 )
+ {
+ var index=0, i=0, iLen=an1.length;
+ var nNode1, nNode2;
+
+ while ( i < iLen ) {
+ nNode1 = an1[i].firstChild;
+ nNode2 = an2 ? an2[i].firstChild : null;
+
+ while ( nNode1 ) {
+ if ( nNode1.nodeType === 1 ) {
+ if ( an2 ) {
+ fn( nNode1, nNode2, index );
+ }
+ else {
+ fn( nNode1, index );
+ }
+
+ index++;
+ }
+
+ nNode1 = nNode1.nextSibling;
+ nNode2 = an2 ? nNode2.nextSibling : null;
+ }
+
+ i++;
+ }
+ }
+
+
+
+ var __re_html_remove = /<.*?>/g;
+
+
+ /**
+ * Calculate the width of columns for the table
+ * @param {object} oSettings dataTables settings object
+ * @memberof DataTable#oApi
+ */
+ function _fnCalculateColumnWidths ( oSettings )
+ {
+ var
+ table = oSettings.nTable,
+ columns = oSettings.aoColumns,
+ scroll = oSettings.oScroll,
+ scrollY = scroll.sY,
+ scrollX = scroll.sX,
+ scrollXInner = scroll.sXInner,
+ columnCount = columns.length,
+ visibleColumns = _fnGetColumns( oSettings, 'bVisible' ),
+ headerCells = $('th', oSettings.nTHead),
+ tableWidthAttr = table.getAttribute('width'), // from DOM element
+ tableContainer = table.parentNode,
+ userInputs = false,
+ i, column, columnIdx, width, outerWidth,
+ browser = oSettings.oBrowser,
+ ie67 = browser.bScrollOversize;
+
+ var styleWidth = table.style.width;
+ if ( styleWidth && styleWidth.indexOf('%') !== -1 ) {
+ tableWidthAttr = styleWidth;
+ }
+
+ /* Convert any user input sizes into pixel sizes */
+ for ( i=0 ; i').appendTo( tmpTable.find('tbody') );
+
+ // Clone the table header and footer - we can't use the header / footer
+ // from the cloned table, since if scrolling is active, the table's
+ // real header and footer are contained in different table tags
+ tmpTable.find('thead, tfoot').remove();
+ tmpTable
+ .append( $(oSettings.nTHead).clone() )
+ .append( $(oSettings.nTFoot).clone() );
+
+ // Remove any assigned widths from the footer (from scrolling)
+ tmpTable.find('tfoot th, tfoot td').css('width', '');
+
+ // Apply custom sizing to the cloned header
+ headerCells = _fnGetUniqueThs( oSettings, tmpTable.find('thead')[0] );
+
+ for ( i=0 ; i ').css( {
+ width: column.sWidthOrig,
+ margin: 0,
+ padding: 0,
+ border: 0,
+ height: 1
+ } ) );
+ }
+ }
+
+ // Find the widest cell for each column and put it into the table
+ if ( oSettings.aoData.length ) {
+ for ( i=0 ; i').css( scrollX || scrollY ?
+ {
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ height: 1,
+ right: 0,
+ overflow: 'hidden'
+ } :
+ {}
+ )
+ .append( tmpTable )
+ .appendTo( tableContainer );
+
+ // When scrolling (X or Y) we want to set the width of the table as
+ // appropriate. However, when not scrolling leave the table width as it
+ // is. This results in slightly different, but I think correct behaviour
+ if ( scrollX && scrollXInner ) {
+ tmpTable.width( scrollXInner );
+ }
+ else if ( scrollX ) {
+ tmpTable.css( 'width', 'auto' );
+ tmpTable.removeAttr('width');
+
+ // If there is no width attribute or style, then allow the table to
+ // collapse
+ if ( tmpTable.width() < tableContainer.clientWidth && tableWidthAttr ) {
+ tmpTable.width( tableContainer.clientWidth );
+ }
+ }
+ else if ( scrollY ) {
+ tmpTable.width( tableContainer.clientWidth );
+ }
+ else if ( tableWidthAttr ) {
+ tmpTable.width( tableWidthAttr );
+ }
+
+ // Get the width of each column in the constructed table - we need to
+ // know the inner width (so it can be assigned to the other table's
+ // cells) and the outer width so we can calculate the full width of the
+ // table. This is safe since DataTables requires a unique cell for each
+ // column, but if ever a header can span multiple columns, this will
+ // need to be modified.
+ var total = 0;
+ for ( i=0 ; i')
+ .css( 'width', _fnStringToCss( width ) )
+ .appendTo( parent || document.body );
+
+ var val = n[0].offsetWidth;
+ n.remove();
+
+ return val;
+ }
+
+
+ /**
+ * Get the widest node
+ * @param {object} settings dataTables settings object
+ * @param {int} colIdx column of interest
+ * @returns {node} widest table node
+ * @memberof DataTable#oApi
+ */
+ function _fnGetWidestNode( settings, colIdx )
+ {
+ var idx = _fnGetMaxLenString( settings, colIdx );
+ if ( idx < 0 ) {
+ return null;
+ }
+
+ var data = settings.aoData[ idx ];
+ return ! data.nTr ? // Might not have been created when deferred rendering
+ $(' ').html( _fnGetCellData( settings, idx, colIdx, 'display' ) )[0] :
+ data.anCells[ colIdx ];
+ }
+
+
+ /**
+ * Get the maximum strlen for each data column
+ * @param {object} settings dataTables settings object
+ * @param {int} colIdx column of interest
+ * @returns {string} max string length for each column
+ * @memberof DataTable#oApi
+ */
+ function _fnGetMaxLenString( settings, colIdx )
+ {
+ var s, max=-1, maxIdx = -1;
+
+ for ( var i=0, ien=settings.aoData.length ; i max ) {
+ max = s.length;
+ maxIdx = i;
+ }
+ }
+
+ return maxIdx;
+ }
+
+
+ /**
+ * Append a CSS unit (only if required) to a string
+ * @param {string} value to css-ify
+ * @returns {string} value with css unit
+ * @memberof DataTable#oApi
+ */
+ function _fnStringToCss( s )
+ {
+ if ( s === null ) {
+ return '0px';
+ }
+
+ if ( typeof s == 'number' ) {
+ return s < 0 ?
+ '0px' :
+ s+'px';
+ }
+
+ // Check it has a unit character already
+ return s.match(/\d$/) ?
+ s+'px' :
+ s;
+ }
+
+
+
+ function _fnSortFlatten ( settings )
+ {
+ var
+ i, iLen, k, kLen,
+ aSort = [],
+ aiOrig = [],
+ aoColumns = settings.aoColumns,
+ aDataSort, iCol, sType, srcCol,
+ fixed = settings.aaSortingFixed,
+ fixedObj = $.isPlainObject( fixed ),
+ nestedSort = [],
+ add = function ( a ) {
+ if ( a.length && ! $.isArray( a[0] ) ) {
+ // 1D array
+ nestedSort.push( a );
+ }
+ else {
+ // 2D array
+ $.merge( nestedSort, a );
+ }
+ };
+
+ // Build the sort array, with pre-fix and post-fix options if they have been
+ // specified
+ if ( $.isArray( fixed ) ) {
+ add( fixed );
+ }
+
+ if ( fixedObj && fixed.pre ) {
+ add( fixed.pre );
+ }
+
+ add( settings.aaSorting );
+
+ if (fixedObj && fixed.post ) {
+ add( fixed.post );
+ }
+
+ for ( i=0 ; iy ? 1 : 0;
+ if ( test !== 0 ) {
+ return sort.dir === 'asc' ? test : -test;
+ }
+ }
+
+ x = aiOrig[a];
+ y = aiOrig[b];
+ return xy ? 1 : 0;
+ } );
+ }
+ else {
+ // Depreciated - remove in 1.11 (providing a plug-in option)
+ // Not all sort types have formatting methods, so we have to call their sorting
+ // methods.
+ displayMaster.sort( function ( a, b ) {
+ var
+ x, y, k, l, test, sort, fn,
+ len=aSort.length,
+ dataA = aoData[a]._aSortData,
+ dataB = aoData[b]._aSortData;
+
+ for ( k=0 ; ky ? 1 : 0;
+ } );
+ }
+ }
+
+ /* Tell the draw function that we have sorted the data */
+ oSettings.bSorted = true;
+ }
+
+
+ function _fnSortAria ( settings )
+ {
+ var label;
+ var nextSort;
+ var columns = settings.aoColumns;
+ var aSort = _fnSortFlatten( settings );
+ var oAria = settings.oLanguage.oAria;
+
+ // ARIA attributes - need to loop all columns, to update all (removing old
+ // attributes as needed)
+ for ( var i=0, iLen=columns.length ; i/g, "" );
+ var th = col.nTh;
+
+ // IE7 is throwing an error when setting these properties with jQuery's
+ // attr() and removeAttr() methods...
+ th.removeAttribute('aria-sort');
+
+ /* In ARIA only the first sorting column can be marked as sorting - no multi-sort option */
+ if ( col.bSortable ) {
+ if ( aSort.length > 0 && aSort[0].col == i ) {
+ th.setAttribute('aria-sort', aSort[0].dir=="asc" ? "ascending" : "descending" );
+ nextSort = asSorting[ aSort[0].index+1 ] || asSorting[0];
+ }
+ else {
+ nextSort = asSorting[0];
+ }
+
+ label = sTitle + ( nextSort === "asc" ?
+ oAria.sSortAscending :
+ oAria.sSortDescending
+ );
+ }
+ else {
+ label = sTitle;
+ }
+
+ th.setAttribute('aria-label', label);
+ }
+ }
+
+
+ /**
+ * Function to run on user sort request
+ * @param {object} settings dataTables settings object
+ * @param {node} attachTo node to attach the handler to
+ * @param {int} colIdx column sorting index
+ * @param {boolean} [append=false] Append the requested sort to the existing
+ * sort if true (i.e. multi-column sort)
+ * @param {function} [callback] callback function
+ * @memberof DataTable#oApi
+ */
+ function _fnSortListener ( settings, colIdx, append, callback )
+ {
+ var col = settings.aoColumns[ colIdx ];
+ var sorting = settings.aaSorting;
+ var asSorting = col.asSorting;
+ var nextSortIdx;
+ var next = function ( a, overflow ) {
+ var idx = a._idx;
+ if ( idx === undefined ) {
+ idx = $.inArray( a[1], asSorting );
+ }
+
+ return idx+1 < asSorting.length ?
+ idx+1 :
+ overflow ?
+ null :
+ 0;
+ };
+
+ // Convert to 2D array if needed
+ if ( typeof sorting[0] === 'number' ) {
+ sorting = settings.aaSorting = [ sorting ];
+ }
+
+ // If appending the sort then we are multi-column sorting
+ if ( append && settings.oFeatures.bSortMulti ) {
+ // Are we already doing some kind of sort on this column?
+ var sortIdx = $.inArray( colIdx, _pluck(sorting, '0') );
+
+ if ( sortIdx !== -1 ) {
+ // Yes, modify the sort
+ nextSortIdx = next( sorting[sortIdx], true );
+
+ if ( nextSortIdx === null && sorting.length === 1 ) {
+ nextSortIdx = 0; // can't remove sorting completely
+ }
+
+ if ( nextSortIdx === null ) {
+ sorting.splice( sortIdx, 1 );
+ }
+ else {
+ sorting[sortIdx][1] = asSorting[ nextSortIdx ];
+ sorting[sortIdx]._idx = nextSortIdx;
+ }
+ }
+ else {
+ // No sort on this column yet
+ sorting.push( [ colIdx, asSorting[0], 0 ] );
+ sorting[sorting.length-1]._idx = 0;
+ }
+ }
+ else if ( sorting.length && sorting[0][0] == colIdx ) {
+ // Single column - already sorting on this column, modify the sort
+ nextSortIdx = next( sorting[0] );
+
+ sorting.length = 1;
+ sorting[0][1] = asSorting[ nextSortIdx ];
+ sorting[0]._idx = nextSortIdx;
+ }
+ else {
+ // Single column - sort only on this column
+ sorting.length = 0;
+ sorting.push( [ colIdx, asSorting[0] ] );
+ sorting[0]._idx = 0;
+ }
+
+ // Run the sort by calling a full redraw
+ _fnReDraw( settings );
+
+ // callback used for async user interaction
+ if ( typeof callback == 'function' ) {
+ callback( settings );
+ }
+ }
+
+
+ /**
+ * Attach a sort handler (click) to a node
+ * @param {object} settings dataTables settings object
+ * @param {node} attachTo node to attach the handler to
+ * @param {int} colIdx column sorting index
+ * @param {function} [callback] callback function
+ * @memberof DataTable#oApi
+ */
+ function _fnSortAttachListener ( settings, attachTo, colIdx, callback )
+ {
+ var col = settings.aoColumns[ colIdx ];
+
+ _fnBindAction( attachTo, {}, function (e) {
+ /* If the column is not sortable - don't to anything */
+ if ( col.bSortable === false ) {
+ return;
+ }
+
+ // If processing is enabled use a timeout to allow the processing
+ // display to be shown - otherwise to it synchronously
+ if ( settings.oFeatures.bProcessing ) {
+ _fnProcessingDisplay( settings, true );
+
+ setTimeout( function() {
+ _fnSortListener( settings, colIdx, e.shiftKey, callback );
+
+ // In server-side processing, the draw callback will remove the
+ // processing display
+ if ( _fnDataSource( settings ) !== 'ssp' ) {
+ _fnProcessingDisplay( settings, false );
+ }
+ }, 0 );
+ }
+ else {
+ _fnSortListener( settings, colIdx, e.shiftKey, callback );
+ }
+ } );
+ }
+
+
+ /**
+ * Set the sorting classes on table's body, Note: it is safe to call this function
+ * when bSort and bSortClasses are false
+ * @param {object} oSettings dataTables settings object
+ * @memberof DataTable#oApi
+ */
+ function _fnSortingClasses( settings )
+ {
+ var oldSort = settings.aLastSort;
+ var sortClass = settings.oClasses.sSortColumn;
+ var sort = _fnSortFlatten( settings );
+ var features = settings.oFeatures;
+ var i, ien, colIdx;
+
+ if ( features.bSort && features.bSortClasses ) {
+ // Remove old sorting classes
+ for ( i=0, ien=oldSort.length ; i 0 && s.time < +new Date() - (duration*1000) ) {
+ callback();
+ return;
+ }
+
+ // Number of columns have changed - all bets are off, no restore of settings
+ if ( s.columns && columns.length !== s.columns.length ) {
+ callback();
+ return;
+ }
+
+ // Store the saved state so it might be accessed at any time
+ settings.oLoadedState = $.extend( true, {}, s );
+
+ // Restore key features - todo - for 1.11 this needs to be done by
+ // subscribed events
+ if ( s.start !== undefined ) {
+ settings._iDisplayStart = s.start;
+ settings.iInitDisplayStart = s.start;
+ }
+ if ( s.length !== undefined ) {
+ settings._iDisplayLength = s.length;
+ }
+
+ // Order
+ if ( s.order !== undefined ) {
+ settings.aaSorting = [];
+ $.each( s.order, function ( i, col ) {
+ settings.aaSorting.push( col[0] >= columns.length ?
+ [ 0, col[1] ] :
+ col
+ );
+ } );
+ }
+
+ // Search
+ if ( s.search !== undefined ) {
+ $.extend( settings.oPreviousSearch, _fnSearchToHung( s.search ) );
+ }
+
+ // Columns
+ //
+ if ( s.columns ) {
+ for ( i=0, ien=s.columns.length ; i= end )
+ {
+ start = end - len;
+ }
+
+ // Keep the start record on the current page
+ start -= (start % len);
+
+ if ( len === -1 || start < 0 )
+ {
+ start = 0;
+ }
+
+ settings._iDisplayStart = start;
+ }
+
+
+ function _fnRenderer( settings, type )
+ {
+ var renderer = settings.renderer;
+ var host = DataTable.ext.renderer[type];
+
+ if ( $.isPlainObject( renderer ) && renderer[type] ) {
+ // Specific renderer for this type. If available use it, otherwise use
+ // the default.
+ return host[renderer[type]] || host._;
+ }
+ else if ( typeof renderer === 'string' ) {
+ // Common renderer - if there is one available for this type use it,
+ // otherwise use the default
+ return host[renderer] || host._;
+ }
+
+ // Use the default
+ return host._;
+ }
+
+
+ /**
+ * Detect the data source being used for the table. Used to simplify the code
+ * a little (ajax) and to make it compress a little smaller.
+ *
+ * @param {object} settings dataTables settings object
+ * @returns {string} Data source
+ * @memberof DataTable#oApi
+ */
+ function _fnDataSource ( settings )
+ {
+ if ( settings.oFeatures.bServerSide ) {
+ return 'ssp';
+ }
+ else if ( settings.ajax || settings.sAjaxSource ) {
+ return 'ajax';
+ }
+ return 'dom';
+ }
+
+
+
+
+ /**
+ * Computed structure of the DataTables API, defined by the options passed to
+ * `DataTable.Api.register()` when building the API.
+ *
+ * The structure is built in order to speed creation and extension of the Api
+ * objects since the extensions are effectively pre-parsed.
+ *
+ * The array is an array of objects with the following structure, where this
+ * base array represents the Api prototype base:
+ *
+ * [
+ * {
+ * name: 'data' -- string - Property name
+ * val: function () {}, -- function - Api method (or undefined if just an object
+ * methodExt: [ ... ], -- array - Array of Api object definitions to extend the method result
+ * propExt: [ ... ] -- array - Array of Api object definitions to extend the property
+ * },
+ * {
+ * name: 'row'
+ * val: {},
+ * methodExt: [ ... ],
+ * propExt: [
+ * {
+ * name: 'data'
+ * val: function () {},
+ * methodExt: [ ... ],
+ * propExt: [ ... ]
+ * },
+ * ...
+ * ]
+ * }
+ * ]
+ *
+ * @type {Array}
+ * @ignore
+ */
+ var __apiStruct = [];
+
+
+ /**
+ * `Array.prototype` reference.
+ *
+ * @type object
+ * @ignore
+ */
+ var __arrayProto = Array.prototype;
+
+
+ /**
+ * Abstraction for `context` parameter of the `Api` constructor to allow it to
+ * take several different forms for ease of use.
+ *
+ * Each of the input parameter types will be converted to a DataTables settings
+ * object where possible.
+ *
+ * @param {string|node|jQuery|object} mixed DataTable identifier. Can be one
+ * of:
+ *
+ * * `string` - jQuery selector. Any DataTables' matching the given selector
+ * with be found and used.
+ * * `node` - `TABLE` node which has already been formed into a DataTable.
+ * * `jQuery` - A jQuery object of `TABLE` nodes.
+ * * `object` - DataTables settings object
+ * * `DataTables.Api` - API instance
+ * @return {array|null} Matching DataTables settings objects. `null` or
+ * `undefined` is returned if no matching DataTable is found.
+ * @ignore
+ */
+ var _toSettings = function ( mixed )
+ {
+ var idx, jq;
+ var settings = DataTable.settings;
+ var tables = $.map( settings, function (el, i) {
+ return el.nTable;
+ } );
+
+ if ( ! mixed ) {
+ return [];
+ }
+ else if ( mixed.nTable && mixed.oApi ) {
+ // DataTables settings object
+ return [ mixed ];
+ }
+ else if ( mixed.nodeName && mixed.nodeName.toLowerCase() === 'table' ) {
+ // Table node
+ idx = $.inArray( mixed, tables );
+ return idx !== -1 ? [ settings[idx] ] : null;
+ }
+ else if ( mixed && typeof mixed.settings === 'function' ) {
+ return mixed.settings().toArray();
+ }
+ else if ( typeof mixed === 'string' ) {
+ // jQuery selector
+ jq = $(mixed);
+ }
+ else if ( mixed instanceof $ ) {
+ // jQuery object (also DataTables instance)
+ jq = mixed;
+ }
+
+ if ( jq ) {
+ return jq.map( function(i) {
+ idx = $.inArray( this, tables );
+ return idx !== -1 ? settings[idx] : null;
+ } ).toArray();
+ }
+ };
+
+
+ /**
+ * DataTables API class - used to control and interface with one or more
+ * DataTables enhanced tables.
+ *
+ * The API class is heavily based on jQuery, presenting a chainable interface
+ * that you can use to interact with tables. Each instance of the API class has
+ * a "context" - i.e. the tables that it will operate on. This could be a single
+ * table, all tables on a page or a sub-set thereof.
+ *
+ * Additionally the API is designed to allow you to easily work with the data in
+ * the tables, retrieving and manipulating it as required. This is done by
+ * presenting the API class as an array like interface. The contents of the
+ * array depend upon the actions requested by each method (for example
+ * `rows().nodes()` will return an array of nodes, while `rows().data()` will
+ * return an array of objects or arrays depending upon your table's
+ * configuration). The API object has a number of array like methods (`push`,
+ * `pop`, `reverse` etc) as well as additional helper methods (`each`, `pluck`,
+ * `unique` etc) to assist your working with the data held in a table.
+ *
+ * Most methods (those which return an Api instance) are chainable, which means
+ * the return from a method call also has all of the methods available that the
+ * top level object had. For example, these two calls are equivalent:
+ *
+ * // Not chained
+ * api.row.add( {...} );
+ * api.draw();
+ *
+ * // Chained
+ * api.row.add( {...} ).draw();
+ *
+ * @class DataTable.Api
+ * @param {array|object|string|jQuery} context DataTable identifier. This is
+ * used to define which DataTables enhanced tables this API will operate on.
+ * Can be one of:
+ *
+ * * `string` - jQuery selector. Any DataTables' matching the given selector
+ * with be found and used.
+ * * `node` - `TABLE` node which has already been formed into a DataTable.
+ * * `jQuery` - A jQuery object of `TABLE` nodes.
+ * * `object` - DataTables settings object
+ * @param {array} [data] Data to initialise the Api instance with.
+ *
+ * @example
+ * // Direct initialisation during DataTables construction
+ * var api = $('#example').DataTable();
+ *
+ * @example
+ * // Initialisation using a DataTables jQuery object
+ * var api = $('#example').dataTable().api();
+ *
+ * @example
+ * // Initialisation as a constructor
+ * var api = new $.fn.DataTable.Api( 'table.dataTable' );
+ */
+ _Api = function ( context, data )
+ {
+ if ( ! (this instanceof _Api) ) {
+ return new _Api( context, data );
+ }
+
+ var settings = [];
+ var ctxSettings = function ( o ) {
+ var a = _toSettings( o );
+ if ( a ) {
+ settings.push.apply( settings, a );
+ }
+ };
+
+ if ( $.isArray( context ) ) {
+ for ( var i=0, ien=context.length ; i idx ?
+ new _Api( ctx[idx], this[idx] ) :
+ null;
+ },
+
+
+ filter: function ( fn )
+ {
+ var a = [];
+
+ if ( __arrayProto.filter ) {
+ a = __arrayProto.filter.call( this, fn, this );
+ }
+ else {
+ // Compatibility for browsers without EMCA-252-5 (JS 1.6)
+ for ( var i=0, ien=this.length ; i 0 ) {
+ return ctx[0].json;
+ }
+
+ // else return undefined;
+ } );
+
+
+ /**
+ * Get the data submitted in the last Ajax request
+ */
+ _api_register( 'ajax.params()', function () {
+ var ctx = this.context;
+
+ if ( ctx.length > 0 ) {
+ return ctx[0].oAjaxData;
+ }
+
+ // else return undefined;
+ } );
+
+
+ /**
+ * Reload tables from the Ajax data source. Note that this function will
+ * automatically re-draw the table when the remote data has been loaded.
+ *
+ * @param {boolean} [reset=true] Reset (default) or hold the current paging
+ * position. A full re-sort and re-filter is performed when this method is
+ * called, which is why the pagination reset is the default action.
+ * @returns {DataTables.Api} this
+ */
+ _api_register( 'ajax.reload()', function ( callback, resetPaging ) {
+ return this.iterator( 'table', function (settings) {
+ __reload( settings, resetPaging===false, callback );
+ } );
+ } );
+
+
+ /**
+ * Get the current Ajax URL. Note that this returns the URL from the first
+ * table in the current context.
+ *
+ * @return {string} Current Ajax source URL
+ *//**
+ * Set the Ajax URL. Note that this will set the URL for all tables in the
+ * current context.
+ *
+ * @param {string} url URL to set.
+ * @returns {DataTables.Api} this
+ */
+ _api_register( 'ajax.url()', function ( url ) {
+ var ctx = this.context;
+
+ if ( url === undefined ) {
+ // get
+ if ( ctx.length === 0 ) {
+ return undefined;
+ }
+ ctx = ctx[0];
+
+ return ctx.ajax ?
+ $.isPlainObject( ctx.ajax ) ?
+ ctx.ajax.url :
+ ctx.ajax :
+ ctx.sAjaxSource;
+ }
+
+ // set
+ return this.iterator( 'table', function ( settings ) {
+ if ( $.isPlainObject( settings.ajax ) ) {
+ settings.ajax.url = url;
+ }
+ else {
+ settings.ajax = url;
+ }
+ // No need to consider sAjaxSource here since DataTables gives priority
+ // to `ajax` over `sAjaxSource`. So setting `ajax` here, renders any
+ // value of `sAjaxSource` redundant.
+ } );
+ } );
+
+
+ /**
+ * Load data from the newly set Ajax URL. Note that this method is only
+ * available when `ajax.url()` is used to set a URL. Additionally, this method
+ * has the same effect as calling `ajax.reload()` but is provided for
+ * convenience when setting a new URL. Like `ajax.reload()` it will
+ * automatically redraw the table once the remote data has been loaded.
+ *
+ * @returns {DataTables.Api} this
+ */
+ _api_register( 'ajax.url().load()', function ( callback, resetPaging ) {
+ // Same as a reload, but makes sense to present it for easy access after a
+ // url change
+ return this.iterator( 'table', function ( ctx ) {
+ __reload( ctx, resetPaging===false, callback );
+ } );
+ } );
+
+
+
+
+ var _selector_run = function ( type, selector, selectFn, settings, opts )
+ {
+ var
+ out = [], res,
+ a, i, ien, j, jen,
+ selectorType = typeof selector;
+
+ // Can't just check for isArray here, as an API or jQuery instance might be
+ // given with their array like look
+ if ( ! selector || selectorType === 'string' || selectorType === 'function' || selector.length === undefined ) {
+ selector = [ selector ];
+ }
+
+ for ( i=0, ien=selector.length ; i 0 ) {
+ // Assign the first element to the first item in the instance
+ // and truncate the instance and context
+ inst[0] = inst[i];
+ inst[0].length = 1;
+ inst.length = 1;
+ inst.context = [ inst.context[i] ];
+
+ return inst;
+ }
+ }
+
+ // Not found - return an empty instance
+ inst.length = 0;
+ return inst;
+ };
+
+
+ var _selector_row_indexes = function ( settings, opts )
+ {
+ var
+ i, ien, tmp, a=[],
+ displayFiltered = settings.aiDisplay,
+ displayMaster = settings.aiDisplayMaster;
+
+ var
+ search = opts.search, // none, applied, removed
+ order = opts.order, // applied, current, index (original - compatibility with 1.9)
+ page = opts.page; // all, current
+
+ if ( _fnDataSource( settings ) == 'ssp' ) {
+ // In server-side processing mode, most options are irrelevant since
+ // rows not shown don't exist and the index order is the applied order
+ // Removed is a special case - for consistency just return an empty
+ // array
+ return search === 'removed' ?
+ [] :
+ _range( 0, displayMaster.length );
+ }
+ else if ( page == 'current' ) {
+ // Current page implies that order=current and fitler=applied, since it is
+ // fairly senseless otherwise, regardless of what order and search actually
+ // are
+ for ( i=settings._iDisplayStart, ien=settings.fnDisplayEnd() ; i= 0 && search == 'applied') )
+ {
+ a.push( i );
+ }
+ }
+ }
+ }
+
+ return a;
+ };
+
+
+ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * Rows
+ *
+ * {} - no selector - use all available rows
+ * {integer} - row aoData index
+ * {node} - TR node
+ * {string} - jQuery selector to apply to the TR elements
+ * {array} - jQuery array of nodes, or simply an array of TR nodes
+ *
+ */
+ var __row_selector = function ( settings, selector, opts )
+ {
+ var rows;
+ var run = function ( sel ) {
+ var selInt = _intVal( sel );
+ var i, ien;
+ var aoData = settings.aoData;
+
+ // Short cut - selector is a number and no options provided (default is
+ // all records, so no need to check if the index is in there, since it
+ // must be - dev error if the index doesn't exist).
+ if ( selInt !== null && ! opts ) {
+ return [ selInt ];
+ }
+
+ if ( ! rows ) {
+ rows = _selector_row_indexes( settings, opts );
+ }
+
+ if ( selInt !== null && $.inArray( selInt, rows ) !== -1 ) {
+ // Selector - integer
+ return [ selInt ];
+ }
+ else if ( sel === null || sel === undefined || sel === '' ) {
+ // Selector - none
+ return rows;
+ }
+
+ // Selector - function
+ if ( typeof sel === 'function' ) {
+ return $.map( rows, function (idx) {
+ var row = aoData[ idx ];
+ return sel( idx, row._aData, row.nTr ) ? idx : null;
+ } );
+ }
+
+ // Selector - node
+ if ( sel.nodeName ) {
+ var rowIdx = sel._DT_RowIndex; // Property added by DT for fast lookup
+ var cellIdx = sel._DT_CellIndex;
+
+ if ( rowIdx !== undefined ) {
+ // Make sure that the row is actually still present in the table
+ return aoData[ rowIdx ] && aoData[ rowIdx ].nTr === sel ?
+ [ rowIdx ] :
+ [];
+ }
+ else if ( cellIdx ) {
+ return aoData[ cellIdx.row ] && aoData[ cellIdx.row ].nTr === sel.parentNode ?
+ [ cellIdx.row ] :
+ [];
+ }
+ else {
+ var host = $(sel).closest('*[data-dt-row]');
+ return host.length ?
+ [ host.data('dt-row') ] :
+ [];
+ }
+ }
+
+ // ID selector. Want to always be able to select rows by id, regardless
+ // of if the tr element has been created or not, so can't rely upon
+ // jQuery here - hence a custom implementation. This does not match
+ // Sizzle's fast selector or HTML4 - in HTML5 the ID can be anything,
+ // but to select it using a CSS selector engine (like Sizzle or
+ // querySelect) it would need to need to be escaped for some characters.
+ // DataTables simplifies this for row selectors since you can select
+ // only a row. A # indicates an id any anything that follows is the id -
+ // unescaped.
+ if ( typeof sel === 'string' && sel.charAt(0) === '#' ) {
+ // get row index from id
+ var rowObj = settings.aIds[ sel.replace( /^#/, '' ) ];
+ if ( rowObj !== undefined ) {
+ return [ rowObj.idx ];
+ }
+
+ // need to fall through to jQuery in case there is DOM id that
+ // matches
+ }
+
+ // Get nodes in the order from the `rows` array with null values removed
+ var nodes = _removeEmpty(
+ _pluck_order( settings.aoData, rows, 'nTr' )
+ );
+
+ // Selector - jQuery selector string, array of nodes or jQuery object/
+ // As jQuery's .filter() allows jQuery objects to be passed in filter,
+ // it also allows arrays, so this will cope with all three options
+ return $(nodes)
+ .filter( sel )
+ .map( function () {
+ return this._DT_RowIndex;
+ } )
+ .toArray();
+ };
+
+ return _selector_run( 'row', selector, run, settings, opts );
+ };
+
+
+ _api_register( 'rows()', function ( selector, opts ) {
+ // argument shifting
+ if ( selector === undefined ) {
+ selector = '';
+ }
+ else if ( $.isPlainObject( selector ) ) {
+ opts = selector;
+ selector = '';
+ }
+
+ opts = _selector_opts( opts );
+
+ var inst = this.iterator( 'table', function ( settings ) {
+ return __row_selector( settings, selector, opts );
+ }, 1 );
+
+ // Want argument shifting here and in __row_selector?
+ inst.selector.rows = selector;
+ inst.selector.opts = opts;
+
+ return inst;
+ } );
+
+ _api_register( 'rows().nodes()', function () {
+ return this.iterator( 'row', function ( settings, row ) {
+ return settings.aoData[ row ].nTr || undefined;
+ }, 1 );
+ } );
+
+ _api_register( 'rows().data()', function () {
+ return this.iterator( true, 'rows', function ( settings, rows ) {
+ return _pluck_order( settings.aoData, rows, '_aData' );
+ }, 1 );
+ } );
+
+ _api_registerPlural( 'rows().cache()', 'row().cache()', function ( type ) {
+ return this.iterator( 'row', function ( settings, row ) {
+ var r = settings.aoData[ row ];
+ return type === 'search' ? r._aFilterData : r._aSortData;
+ }, 1 );
+ } );
+
+ _api_registerPlural( 'rows().invalidate()', 'row().invalidate()', function ( src ) {
+ return this.iterator( 'row', function ( settings, row ) {
+ _fnInvalidate( settings, row, src );
+ } );
+ } );
+
+ _api_registerPlural( 'rows().indexes()', 'row().index()', function () {
+ return this.iterator( 'row', function ( settings, row ) {
+ return row;
+ }, 1 );
+ } );
+
+ _api_registerPlural( 'rows().ids()', 'row().id()', function ( hash ) {
+ var a = [];
+ var context = this.context;
+
+ // `iterator` will drop undefined values, but in this case we want them
+ for ( var i=0, ien=context.length ; i 0 ) {
+ settings._iRecordsDisplay--;
+ }
+
+ // Check for an 'overflow' they case for displaying the table
+ _fnLengthOverflow( settings );
+
+ // Remove the row's ID reference if there is one
+ var id = settings.rowIdFn( rowData._aData );
+ if ( id !== undefined ) {
+ delete settings.aIds[ id ];
+ }
+ } );
+
+ this.iterator( 'table', function ( settings ) {
+ for ( var i=0, ien=settings.aoData.length ; i ').addClass( k );
+ $('td', created)
+ .addClass( k )
+ .html( r )
+ [0].colSpan = _fnVisbleColumns( ctx );
+
+ rows.push( created[0] );
+ }
+ };
+
+ addRow( data, klass );
+
+ if ( row._details ) {
+ row._details.detach();
+ }
+
+ row._details = $(rows);
+
+ // If the children were already shown, that state should be retained
+ if ( row._detailsShow ) {
+ row._details.insertAfter( row.nTr );
+ }
+ };
+
+
+ var __details_remove = function ( api, idx )
+ {
+ var ctx = api.context;
+
+ if ( ctx.length ) {
+ var row = ctx[0].aoData[ idx !== undefined ? idx : api[0] ];
+
+ if ( row && row._details ) {
+ row._details.remove();
+
+ row._detailsShow = undefined;
+ row._details = undefined;
+ }
+ }
+ };
+
+
+ var __details_display = function ( api, show ) {
+ var ctx = api.context;
+
+ if ( ctx.length && api.length ) {
+ var row = ctx[0].aoData[ api[0] ];
+
+ if ( row._details ) {
+ row._detailsShow = show;
+
+ if ( show ) {
+ row._details.insertAfter( row.nTr );
+ }
+ else {
+ row._details.detach();
+ }
+
+ __details_events( ctx[0] );
+ }
+ }
+ };
+
+
+ var __details_events = function ( settings )
+ {
+ var api = new _Api( settings );
+ var namespace = '.dt.DT_details';
+ var drawEvent = 'draw'+namespace;
+ var colvisEvent = 'column-visibility'+namespace;
+ var destroyEvent = 'destroy'+namespace;
+ var data = settings.aoData;
+
+ api.off( drawEvent +' '+ colvisEvent +' '+ destroyEvent );
+
+ if ( _pluck( data, '_details' ).length > 0 ) {
+ // On each draw, insert the required elements into the document
+ api.on( drawEvent, function ( e, ctx ) {
+ if ( settings !== ctx ) {
+ return;
+ }
+
+ api.rows( {page:'current'} ).eq(0).each( function (idx) {
+ // Internal data grab
+ var row = data[ idx ];
+
+ if ( row._detailsShow ) {
+ row._details.insertAfter( row.nTr );
+ }
+ } );
+ } );
+
+ // Column visibility change - update the colspan
+ api.on( colvisEvent, function ( e, ctx, idx, vis ) {
+ if ( settings !== ctx ) {
+ return;
+ }
+
+ // Update the colspan for the details rows (note, only if it already has
+ // a colspan)
+ var row, visible = _fnVisbleColumns( ctx );
+
+ for ( var i=0, ien=data.length ; i=0 count from left, <0 count from right)
+ * "{integer}:visIdx" - visible column index (i.e. translate to column index) (>=0 count from left, <0 count from right)
+ * "{integer}:visible" - alias for {integer}:visIdx (>=0 count from left, <0 count from right)
+ * "{string}:name" - column name
+ * "{string}" - jQuery selector on column header nodes
+ *
+ */
+
+ // can be an array of these items, comma separated list, or an array of comma
+ // separated lists
+
+ var __re_column_selector = /^([^:]+):(name|visIdx|visible)$/;
+
+
+ // r1 and r2 are redundant - but it means that the parameters match for the
+ // iterator callback in columns().data()
+ var __columnData = function ( settings, column, r1, r2, rows ) {
+ var a = [];
+ for ( var row=0, ien=rows.length ; row= 0 ?
+ selInt : // Count from left
+ columns.length + selInt // Count from right (+ because its a negative value)
+ ];
+ }
+
+ // Selector = function
+ if ( typeof s === 'function' ) {
+ var rows = _selector_row_indexes( settings, opts );
+
+ return $.map( columns, function (col, idx) {
+ return s(
+ idx,
+ __columnData( settings, idx, 0, 0, rows ),
+ nodes[ idx ]
+ ) ? idx : null;
+ } );
+ }
+
+ // jQuery or string selector
+ var match = typeof s === 'string' ?
+ s.match( __re_column_selector ) :
+ '';
+
+ if ( match ) {
+ switch( match[2] ) {
+ case 'visIdx':
+ case 'visible':
+ var idx = parseInt( match[1], 10 );
+ // Visible index given, convert to column index
+ if ( idx < 0 ) {
+ // Counting from the right
+ var visColumns = $.map( columns, function (col,i) {
+ return col.bVisible ? i : null;
+ } );
+ return [ visColumns[ visColumns.length + idx ] ];
+ }
+ // Counting from the left
+ return [ _fnVisibleToColumnIndex( settings, idx ) ];
+
+ case 'name':
+ // match by name. `names` is column index complete and in order
+ return $.map( names, function (name, i) {
+ return name === match[1] ? i : null;
+ } );
+
+ default:
+ return [];
+ }
+ }
+
+ // Cell in the table body
+ if ( s.nodeName && s._DT_CellIndex ) {
+ return [ s._DT_CellIndex.column ];
+ }
+
+ // jQuery selector on the TH elements for the columns
+ var jqResult = $( nodes )
+ .filter( s )
+ .map( function () {
+ return $.inArray( this, nodes ); // `nodes` is column index complete and in order
+ } )
+ .toArray();
+
+ if ( jqResult.length || ! s.nodeName ) {
+ return jqResult;
+ }
+
+ // Otherwise a node which might have a `dt-column` data attribute, or be
+ // a child or such an element
+ var host = $(s).closest('*[data-dt-column]');
+ return host.length ?
+ [ host.data('dt-column') ] :
+ [];
+ };
+
+ return _selector_run( 'column', selector, run, settings, opts );
+ };
+
+
+ var __setColumnVis = function ( settings, column, vis ) {
+ var
+ cols = settings.aoColumns,
+ col = cols[ column ],
+ data = settings.aoData,
+ row, cells, i, ien, tr;
+
+ // Get
+ if ( vis === undefined ) {
+ return col.bVisible;
+ }
+
+ // Set
+ // No change
+ if ( col.bVisible === vis ) {
+ return;
+ }
+
+ if ( vis ) {
+ // Insert column
+ // Need to decide if we should use appendChild or insertBefore
+ var insertBefore = $.inArray( true, _pluck(cols, 'bVisible'), column+1 );
+
+ for ( i=0, ien=data.length ; i iThat;
+ }
+
+ return true;
+ };
+
+
+ /**
+ * Check if a `` node is a DataTable table already or not.
+ *
+ * @param {node|jquery|string} table Table node, jQuery object or jQuery
+ * selector for the table to test. Note that if more than more than one
+ * table is passed on, only the first will be checked
+ * @returns {boolean} true the table given is a DataTable, or false otherwise
+ * @static
+ * @dtopt API-Static
+ *
+ * @example
+ * if ( ! $.fn.DataTable.isDataTable( '#example' ) ) {
+ * $('#example').dataTable();
+ * }
+ */
+ DataTable.isDataTable = DataTable.fnIsDataTable = function ( table )
+ {
+ var t = $(table).get(0);
+ var is = false;
+
+ if ( table instanceof DataTable.Api ) {
+ return true;
+ }
+
+ $.each( DataTable.settings, function (i, o) {
+ var head = o.nScrollHead ? $('table', o.nScrollHead)[0] : null;
+ var foot = o.nScrollFoot ? $('table', o.nScrollFoot)[0] : null;
+
+ if ( o.nTable === t || head === t || foot === t ) {
+ is = true;
+ }
+ } );
+
+ return is;
+ };
+
+
+ /**
+ * Get all DataTable tables that have been initialised - optionally you can
+ * select to get only currently visible tables.
+ *
+ * @param {boolean} [visible=false] Flag to indicate if you want all (default)
+ * or visible tables only.
+ * @returns {array} Array of `table` nodes (not DataTable instances) which are
+ * DataTables
+ * @static
+ * @dtopt API-Static
+ *
+ * @example
+ * $.each( $.fn.dataTable.tables(true), function () {
+ * $(table).DataTable().columns.adjust();
+ * } );
+ */
+ DataTable.tables = DataTable.fnTables = function ( visible )
+ {
+ var api = false;
+
+ if ( $.isPlainObject( visible ) ) {
+ api = visible.api;
+ visible = visible.visible;
+ }
+
+ var a = $.map( DataTable.settings, function (o) {
+ if ( !visible || (visible && $(o.nTable).is(':visible')) ) {
+ return o.nTable;
+ }
+ } );
+
+ return api ?
+ new _Api( a ) :
+ a;
+ };
+
+
+ /**
+ * Convert from camel case parameters to Hungarian notation. This is made public
+ * for the extensions to provide the same ability as DataTables core to accept
+ * either the 1.9 style Hungarian notation, or the 1.10+ style camelCase
+ * parameters.
+ *
+ * @param {object} src The model object which holds all parameters that can be
+ * mapped.
+ * @param {object} user The object to convert from camel case to Hungarian.
+ * @param {boolean} force When set to `true`, properties which already have a
+ * Hungarian value in the `user` object will be overwritten. Otherwise they
+ * won't be.
+ */
+ DataTable.camelToHungarian = _fnCamelToHungarian;
+
+
+
+ /**
+ *
+ */
+ _api_register( '$()', function ( selector, opts ) {
+ var
+ rows = this.rows( opts ).nodes(), // Get all rows
+ jqRows = $(rows);
+
+ return $( [].concat(
+ jqRows.filter( selector ).toArray(),
+ jqRows.find( selector ).toArray()
+ ) );
+ } );
+
+
+ // jQuery functions to operate on the tables
+ $.each( [ 'on', 'one', 'off' ], function (i, key) {
+ _api_register( key+'()', function ( /* event, handler */ ) {
+ var args = Array.prototype.slice.call(arguments);
+
+ // Add the `dt` namespace automatically if it isn't already present
+ args[0] = $.map( args[0].split( /\s/ ), function ( e ) {
+ return ! e.match(/\.dt\b/) ?
+ e+'.dt' :
+ e;
+ } ).join( ' ' );
+
+ var inst = $( this.tables().nodes() );
+ inst[key].apply( inst, args );
+ return this;
+ } );
+ } );
+
+
+ _api_register( 'clear()', function () {
+ return this.iterator( 'table', function ( settings ) {
+ _fnClearTable( settings );
+ } );
+ } );
+
+
+ _api_register( 'settings()', function () {
+ return new _Api( this.context, this.context );
+ } );
+
+
+ _api_register( 'init()', function () {
+ var ctx = this.context;
+ return ctx.length ? ctx[0].oInit : null;
+ } );
+
+
+ _api_register( 'data()', function () {
+ return this.iterator( 'table', function ( settings ) {
+ return _pluck( settings.aoData, '_aData' );
+ } ).flatten();
+ } );
+
+
+ _api_register( 'destroy()', function ( remove ) {
+ remove = remove || false;
+
+ return this.iterator( 'table', function ( settings ) {
+ var orig = settings.nTableWrapper.parentNode;
+ var classes = settings.oClasses;
+ var table = settings.nTable;
+ var tbody = settings.nTBody;
+ var thead = settings.nTHead;
+ var tfoot = settings.nTFoot;
+ var jqTable = $(table);
+ var jqTbody = $(tbody);
+ var jqWrapper = $(settings.nTableWrapper);
+ var rows = $.map( settings.aoData, function (r) { return r.nTr; } );
+ var i, ien;
+
+ // Flag to note that the table is currently being destroyed - no action
+ // should be taken
+ settings.bDestroying = true;
+
+ // Fire off the destroy callbacks for plug-ins etc
+ _fnCallbackFire( settings, "aoDestroyCallback", "destroy", [settings] );
+
+ // If not being removed from the document, make all columns visible
+ if ( ! remove ) {
+ new _Api( settings ).columns().visible( true );
+ }
+
+ // Blitz all `DT` namespaced events (these are internal events, the
+ // lowercase, `dt` events are user subscribed and they are responsible
+ // for removing them
+ jqWrapper.off('.DT').find(':not(tbody *)').off('.DT');
+ $(window).off('.DT-'+settings.sInstance);
+
+ // When scrolling we had to break the table up - restore it
+ if ( table != thead.parentNode ) {
+ jqTable.children('thead').detach();
+ jqTable.append( thead );
+ }
+
+ if ( tfoot && table != tfoot.parentNode ) {
+ jqTable.children('tfoot').detach();
+ jqTable.append( tfoot );
+ }
+
+ settings.aaSorting = [];
+ settings.aaSortingFixed = [];
+ _fnSortingClasses( settings );
+
+ $( rows ).removeClass( settings.asStripeClasses.join(' ') );
+
+ $('th, td', thead).removeClass( classes.sSortable+' '+
+ classes.sSortableAsc+' '+classes.sSortableDesc+' '+classes.sSortableNone
+ );
+
+ // Add the TR elements back into the table in their original order
+ jqTbody.children().detach();
+ jqTbody.append( rows );
+
+ // Remove the DataTables generated nodes, events and classes
+ var removedMethod = remove ? 'remove' : 'detach';
+ jqTable[ removedMethod ]();
+ jqWrapper[ removedMethod ]();
+
+ // If we need to reattach the table to the document
+ if ( ! remove && orig ) {
+ // insertBefore acts like appendChild if !arg[1]
+ orig.insertBefore( table, settings.nTableReinsertBefore );
+
+ // Restore the width of the original table - was read from the style property,
+ // so we can restore directly to that
+ jqTable
+ .css( 'width', settings.sDestroyWidth )
+ .removeClass( classes.sTable );
+
+ // If the were originally stripe classes - then we add them back here.
+ // Note this is not fool proof (for example if not all rows had stripe
+ // classes - but it's a good effort without getting carried away
+ ien = settings.asDestroyStripes.length;
+
+ if ( ien ) {
+ jqTbody.children().each( function (i) {
+ $(this).addClass( settings.asDestroyStripes[i % ien] );
+ } );
+ }
+ }
+
+ /* Remove the settings object from the settings array */
+ var idx = $.inArray( settings, DataTable.settings );
+ if ( idx !== -1 ) {
+ DataTable.settings.splice( idx, 1 );
+ }
+ } );
+ } );
+
+
+ // Add the `every()` method for rows, columns and cells in a compact form
+ $.each( [ 'column', 'row', 'cell' ], function ( i, type ) {
+ _api_register( type+'s().every()', function ( fn ) {
+ var opts = this.selector.opts;
+ var api = this;
+
+ return this.iterator( type, function ( settings, arg1, arg2, arg3, arg4 ) {
+ // Rows and columns:
+ // arg1 - index
+ // arg2 - table counter
+ // arg3 - loop counter
+ // arg4 - undefined
+ // Cells:
+ // arg1 - row index
+ // arg2 - column index
+ // arg3 - table counter
+ // arg4 - loop counter
+ fn.call(
+ api[ type ](
+ arg1,
+ type==='cell' ? arg2 : opts,
+ type==='cell' ? opts : undefined
+ ),
+ arg1, arg2, arg3, arg4
+ );
+ } );
+ } );
+ } );
+
+
+ // i18n method for extensions to be able to use the language object from the
+ // DataTable
+ _api_register( 'i18n()', function ( token, def, plural ) {
+ var ctx = this.context[0];
+ var resolved = _fnGetObjectDataFn( token )( ctx.oLanguage );
+
+ if ( resolved === undefined ) {
+ resolved = def;
+ }
+
+ if ( plural !== undefined && $.isPlainObject( resolved ) ) {
+ resolved = resolved[ plural ] !== undefined ?
+ resolved[ plural ] :
+ resolved._;
+ }
+
+ return resolved.replace( '%d', plural ); // nb: plural might be undefined,
+ } );
+ /**
+ * Version string for plug-ins to check compatibility. Allowed format is
+ * `a.b.c-d` where: a:int, b:int, c:int, d:string(dev|beta|alpha). `d` is used
+ * only for non-release builds. See http://semver.org/ for more information.
+ * @member
+ * @type string
+ * @default Version number
+ */
+ DataTable.version = "1.10.20";
+
+ /**
+ * Private data store, containing all of the settings objects that are
+ * created for the tables on a given page.
+ *
+ * Note that the `DataTable.settings` object is aliased to
+ * `jQuery.fn.dataTableExt` through which it may be accessed and
+ * manipulated, or `jQuery.fn.dataTable.settings`.
+ * @member
+ * @type array
+ * @default []
+ * @private
+ */
+ DataTable.settings = [];
+
+ /**
+ * Object models container, for the various models that DataTables has
+ * available to it. These models define the objects that are used to hold
+ * the active state and configuration of the table.
+ * @namespace
+ */
+ DataTable.models = {};
+
+
+
+ /**
+ * Template object for the way in which DataTables holds information about
+ * search information for the global filter and individual column filters.
+ * @namespace
+ */
+ DataTable.models.oSearch = {
+ /**
+ * Flag to indicate if the filtering should be case insensitive or not
+ * @type boolean
+ * @default true
+ */
+ "bCaseInsensitive": true,
+
+ /**
+ * Applied search term
+ * @type string
+ * @default Empty string
+ */
+ "sSearch": "",
+
+ /**
+ * Flag to indicate if the search term should be interpreted as a
+ * regular expression (true) or not (false) and therefore and special
+ * regex characters escaped.
+ * @type boolean
+ * @default false
+ */
+ "bRegex": false,
+
+ /**
+ * Flag to indicate if DataTables is to use its smart filtering or not.
+ * @type boolean
+ * @default true
+ */
+ "bSmart": true
+ };
+
+
+
+
+ /**
+ * Template object for the way in which DataTables holds information about
+ * each individual row. This is the object format used for the settings
+ * aoData array.
+ * @namespace
+ */
+ DataTable.models.oRow = {
+ /**
+ * TR element for the row
+ * @type node
+ * @default null
+ */
+ "nTr": null,
+
+ /**
+ * Array of TD elements for each row. This is null until the row has been
+ * created.
+ * @type array nodes
+ * @default []
+ */
+ "anCells": null,
+
+ /**
+ * Data object from the original data source for the row. This is either
+ * an array if using the traditional form of DataTables, or an object if
+ * using mData options. The exact type will depend on the passed in
+ * data from the data source, or will be an array if using DOM a data
+ * source.
+ * @type array|object
+ * @default []
+ */
+ "_aData": [],
+
+ /**
+ * Sorting data cache - this array is ostensibly the same length as the
+ * number of columns (although each index is generated only as it is
+ * needed), and holds the data that is used for sorting each column in the
+ * row. We do this cache generation at the start of the sort in order that
+ * the formatting of the sort data need be done only once for each cell
+ * per sort. This array should not be read from or written to by anything
+ * other than the master sorting methods.
+ * @type array
+ * @default null
+ * @private
+ */
+ "_aSortData": null,
+
+ /**
+ * Per cell filtering data cache. As per the sort data cache, used to
+ * increase the performance of the filtering in DataTables
+ * @type array
+ * @default null
+ * @private
+ */
+ "_aFilterData": null,
+
+ /**
+ * Filtering data cache. This is the same as the cell filtering cache, but
+ * in this case a string rather than an array. This is easily computed with
+ * a join on `_aFilterData`, but is provided as a cache so the join isn't
+ * needed on every search (memory traded for performance)
+ * @type array
+ * @default null
+ * @private
+ */
+ "_sFilterRow": null,
+
+ /**
+ * Cache of the class name that DataTables has applied to the row, so we
+ * can quickly look at this variable rather than needing to do a DOM check
+ * on className for the nTr property.
+ * @type string
+ * @default Empty string
+ * @private
+ */
+ "_sRowStripe": "",
+
+ /**
+ * Denote if the original data source was from the DOM, or the data source
+ * object. This is used for invalidating data, so DataTables can
+ * automatically read data from the original source, unless uninstructed
+ * otherwise.
+ * @type string
+ * @default null
+ * @private
+ */
+ "src": null,
+
+ /**
+ * Index in the aoData array. This saves an indexOf lookup when we have the
+ * object, but want to know the index
+ * @type integer
+ * @default -1
+ * @private
+ */
+ "idx": -1
+ };
+
+
+ /**
+ * Template object for the column information object in DataTables. This object
+ * is held in the settings aoColumns array and contains all the information that
+ * DataTables needs about each individual column.
+ *
+ * Note that this object is related to {@link DataTable.defaults.column}
+ * but this one is the internal data store for DataTables's cache of columns.
+ * It should NOT be manipulated outside of DataTables. Any configuration should
+ * be done through the initialisation options.
+ * @namespace
+ */
+ DataTable.models.oColumn = {
+ /**
+ * Column index. This could be worked out on-the-fly with $.inArray, but it
+ * is faster to just hold it as a variable
+ * @type integer
+ * @default null
+ */
+ "idx": null,
+
+ /**
+ * A list of the columns that sorting should occur on when this column
+ * is sorted. That this property is an array allows multi-column sorting
+ * to be defined for a column (for example first name / last name columns
+ * would benefit from this). The values are integers pointing to the
+ * columns to be sorted on (typically it will be a single integer pointing
+ * at itself, but that doesn't need to be the case).
+ * @type array
+ */
+ "aDataSort": null,
+
+ /**
+ * Define the sorting directions that are applied to the column, in sequence
+ * as the column is repeatedly sorted upon - i.e. the first value is used
+ * as the sorting direction when the column if first sorted (clicked on).
+ * Sort it again (click again) and it will move on to the next index.
+ * Repeat until loop.
+ * @type array
+ */
+ "asSorting": null,
+
+ /**
+ * Flag to indicate if the column is searchable, and thus should be included
+ * in the filtering or not.
+ * @type boolean
+ */
+ "bSearchable": null,
+
+ /**
+ * Flag to indicate if the column is sortable or not.
+ * @type boolean
+ */
+ "bSortable": null,
+
+ /**
+ * Flag to indicate if the column is currently visible in the table or not
+ * @type boolean
+ */
+ "bVisible": null,
+
+ /**
+ * Store for manual type assignment using the `column.type` option. This
+ * is held in store so we can manipulate the column's `sType` property.
+ * @type string
+ * @default null
+ * @private
+ */
+ "_sManualType": null,
+
+ /**
+ * Flag to indicate if HTML5 data attributes should be used as the data
+ * source for filtering or sorting. True is either are.
+ * @type boolean
+ * @default false
+ * @private
+ */
+ "_bAttrSrc": false,
+
+ /**
+ * Developer definable function that is called whenever a cell is created (Ajax source,
+ * etc) or processed for input (DOM source). This can be used as a compliment to mRender
+ * allowing you to modify the DOM element (add background colour for example) when the
+ * element is available.
+ * @type function
+ * @param {element} nTd The TD node that has been created
+ * @param {*} sData The Data for the cell
+ * @param {array|object} oData The data for the whole row
+ * @param {int} iRow The row index for the aoData data store
+ * @default null
+ */
+ "fnCreatedCell": null,
+
+ /**
+ * Function to get data from a cell in a column. You should never
+ * access data directly through _aData internally in DataTables - always use
+ * the method attached to this property. It allows mData to function as
+ * required. This function is automatically assigned by the column
+ * initialisation method
+ * @type function
+ * @param {array|object} oData The data array/object for the array
+ * (i.e. aoData[]._aData)
+ * @param {string} sSpecific The specific data type you want to get -
+ * 'display', 'type' 'filter' 'sort'
+ * @returns {*} The data for the cell from the given row's data
+ * @default null
+ */
+ "fnGetData": null,
+
+ /**
+ * Function to set data for a cell in the column. You should never
+ * set the data directly to _aData internally in DataTables - always use
+ * this method. It allows mData to function as required. This function
+ * is automatically assigned by the column initialisation method
+ * @type function
+ * @param {array|object} oData The data array/object for the array
+ * (i.e. aoData[]._aData)
+ * @param {*} sValue Value to set
+ * @default null
+ */
+ "fnSetData": null,
+
+ /**
+ * Property to read the value for the cells in the column from the data
+ * source array / object. If null, then the default content is used, if a
+ * function is given then the return from the function is used.
+ * @type function|int|string|null
+ * @default null
+ */
+ "mData": null,
+
+ /**
+ * Partner property to mData which is used (only when defined) to get
+ * the data - i.e. it is basically the same as mData, but without the
+ * 'set' option, and also the data fed to it is the result from mData.
+ * This is the rendering method to match the data method of mData.
+ * @type function|int|string|null
+ * @default null
+ */
+ "mRender": null,
+
+ /**
+ * Unique header TH/TD element for this column - this is what the sorting
+ * listener is attached to (if sorting is enabled.)
+ * @type node
+ * @default null
+ */
+ "nTh": null,
+
+ /**
+ * Unique footer TH/TD element for this column (if there is one). Not used
+ * in DataTables as such, but can be used for plug-ins to reference the
+ * footer for each column.
+ * @type node
+ * @default null
+ */
+ "nTf": null,
+
+ /**
+ * The class to apply to all TD elements in the table's TBODY for the column
+ * @type string
+ * @default null
+ */
+ "sClass": null,
+
+ /**
+ * When DataTables calculates the column widths to assign to each column,
+ * it finds the longest string in each column and then constructs a
+ * temporary table and reads the widths from that. The problem with this
+ * is that "mmm" is much wider then "iiii", but the latter is a longer
+ * string - thus the calculation can go wrong (doing it properly and putting
+ * it into an DOM object and measuring that is horribly(!) slow). Thus as
+ * a "work around" we provide this option. It will append its value to the
+ * text that is found to be the longest string for the column - i.e. padding.
+ * @type string
+ */
+ "sContentPadding": null,
+
+ /**
+ * Allows a default value to be given for a column's data, and will be used
+ * whenever a null data source is encountered (this can be because mData
+ * is set to null, or because the data source itself is null).
+ * @type string
+ * @default null
+ */
+ "sDefaultContent": null,
+
+ /**
+ * Name for the column, allowing reference to the column by name as well as
+ * by index (needs a lookup to work by name).
+ * @type string
+ */
+ "sName": null,
+
+ /**
+ * Custom sorting data type - defines which of the available plug-ins in
+ * afnSortData the custom sorting will use - if any is defined.
+ * @type string
+ * @default std
+ */
+ "sSortDataType": 'std',
+
+ /**
+ * Class to be applied to the header element when sorting on this column
+ * @type string
+ * @default null
+ */
+ "sSortingClass": null,
+
+ /**
+ * Class to be applied to the header element when sorting on this column -
+ * when jQuery UI theming is used.
+ * @type string
+ * @default null
+ */
+ "sSortingClassJUI": null,
+
+ /**
+ * Title of the column - what is seen in the TH element (nTh).
+ * @type string
+ */
+ "sTitle": null,
+
+ /**
+ * Column sorting and filtering type
+ * @type string
+ * @default null
+ */
+ "sType": null,
+
+ /**
+ * Width of the column
+ * @type string
+ * @default null
+ */
+ "sWidth": null,
+
+ /**
+ * Width of the column when it was first "encountered"
+ * @type string
+ * @default null
+ */
+ "sWidthOrig": null
+ };
+
+
+ /*
+ * Developer note: The properties of the object below are given in Hungarian
+ * notation, that was used as the interface for DataTables prior to v1.10, however
+ * from v1.10 onwards the primary interface is camel case. In order to avoid
+ * breaking backwards compatibility utterly with this change, the Hungarian
+ * version is still, internally the primary interface, but is is not documented
+ * - hence the @name tags in each doc comment. This allows a Javascript function
+ * to create a map from Hungarian notation to camel case (going the other direction
+ * would require each property to be listed, which would at around 3K to the size
+ * of DataTables, while this method is about a 0.5K hit.
+ *
+ * Ultimately this does pave the way for Hungarian notation to be dropped
+ * completely, but that is a massive amount of work and will break current
+ * installs (therefore is on-hold until v2).
+ */
+
+ /**
+ * Initialisation options that can be given to DataTables at initialisation
+ * time.
+ * @namespace
+ */
+ DataTable.defaults = {
+ /**
+ * An array of data to use for the table, passed in at initialisation which
+ * will be used in preference to any data which is already in the DOM. This is
+ * particularly useful for constructing tables purely in Javascript, for
+ * example with a custom Ajax call.
+ * @type array
+ * @default null
+ *
+ * @dtopt Option
+ * @name DataTable.defaults.data
+ *
+ * @example
+ * // Using a 2D array data source
+ * $(document).ready( function () {
+ * $('#example').dataTable( {
+ * "data": [
+ * ['Trident', 'Internet Explorer 4.0', 'Win 95+', 4, 'X'],
+ * ['Trident', 'Internet Explorer 5.0', 'Win 95+', 5, 'C'],
+ * ],
+ * "columns": [
+ * { "title": "Engine" },
+ * { "title": "Browser" },
+ * { "title": "Platform" },
+ * { "title": "Version" },
+ * { "title": "Grade" }
+ * ]
+ * } );
+ * } );
+ *
+ * @example
+ * // Using an array of objects as a data source (`data`)
+ * $(document).ready( function () {
+ * $('#example').dataTable( {
+ * "data": [
+ * {
+ * "engine": "Trident",
+ * "browser": "Internet Explorer 4.0",
+ * "platform": "Win 95+",
+ * "version": 4,
+ * "grade": "X"
+ * },
+ * {
+ * "engine": "Trident",
+ * "browser": "Internet Explorer 5.0",
+ * "platform": "Win 95+",
+ * "version": 5,
+ * "grade": "C"
+ * }
+ * ],
+ * "columns": [
+ * { "title": "Engine", "data": "engine" },
+ * { "title": "Browser", "data": "browser" },
+ * { "title": "Platform", "data": "platform" },
+ * { "title": "Version", "data": "version" },
+ * { "title": "Grade", "data": "grade" }
+ * ]
+ * } );
+ * } );
+ */
+ "aaData": null,
+
+
+ /**
+ * If ordering is enabled, then DataTables will perform a first pass sort on
+ * initialisation. You can define which column(s) the sort is performed
+ * upon, and the sorting direction, with this variable. The `sorting` array
+ * should contain an array for each column to be sorted initially containing
+ * the column's index and a direction string ('asc' or 'desc').
+ * @type array
+ * @default [[0,'asc']]
+ *
+ * @dtopt Option
+ * @name DataTable.defaults.order
+ *
+ * @example
+ * // Sort by 3rd column first, and then 4th column
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "order": [[2,'asc'], [3,'desc']]
+ * } );
+ * } );
+ *
+ * // No initial sorting
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "order": []
+ * } );
+ * } );
+ */
+ "aaSorting": [[0,'asc']],
+
+
+ /**
+ * This parameter is basically identical to the `sorting` parameter, but
+ * cannot be overridden by user interaction with the table. What this means
+ * is that you could have a column (visible or hidden) which the sorting
+ * will always be forced on first - any sorting after that (from the user)
+ * will then be performed as required. This can be useful for grouping rows
+ * together.
+ * @type array
+ * @default null
+ *
+ * @dtopt Option
+ * @name DataTable.defaults.orderFixed
+ *
+ * @example
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "orderFixed": [[0,'asc']]
+ * } );
+ * } )
+ */
+ "aaSortingFixed": [],
+
+
+ /**
+ * DataTables can be instructed to load data to display in the table from a
+ * Ajax source. This option defines how that Ajax call is made and where to.
+ *
+ * The `ajax` property has three different modes of operation, depending on
+ * how it is defined. These are:
+ *
+ * * `string` - Set the URL from where the data should be loaded from.
+ * * `object` - Define properties for `jQuery.ajax`.
+ * * `function` - Custom data get function
+ *
+ * `string`
+ * --------
+ *
+ * As a string, the `ajax` property simply defines the URL from which
+ * DataTables will load data.
+ *
+ * `object`
+ * --------
+ *
+ * As an object, the parameters in the object are passed to
+ * [jQuery.ajax](http://api.jquery.com/jQuery.ajax/) allowing fine control
+ * of the Ajax request. DataTables has a number of default parameters which
+ * you can override using this option. Please refer to the jQuery
+ * documentation for a full description of the options available, although
+ * the following parameters provide additional options in DataTables or
+ * require special consideration:
+ *
+ * * `data` - As with jQuery, `data` can be provided as an object, but it
+ * can also be used as a function to manipulate the data DataTables sends
+ * to the server. The function takes a single parameter, an object of
+ * parameters with the values that DataTables has readied for sending. An
+ * object may be returned which will be merged into the DataTables
+ * defaults, or you can add the items to the object that was passed in and
+ * not return anything from the function. This supersedes `fnServerParams`
+ * from DataTables 1.9-.
+ *
+ * * `dataSrc` - By default DataTables will look for the property `data` (or
+ * `aaData` for compatibility with DataTables 1.9-) when obtaining data
+ * from an Ajax source or for server-side processing - this parameter
+ * allows that property to be changed. You can use Javascript dotted
+ * object notation to get a data source for multiple levels of nesting, or
+ * it my be used as a function. As a function it takes a single parameter,
+ * the JSON returned from the server, which can be manipulated as
+ * required, with the returned value being that used by DataTables as the
+ * data source for the table. This supersedes `sAjaxDataProp` from
+ * DataTables 1.9-.
+ *
+ * * `success` - Should not be overridden it is used internally in
+ * DataTables. To manipulate / transform the data returned by the server
+ * use `ajax.dataSrc`, or use `ajax` as a function (see below).
+ *
+ * `function`
+ * ----------
+ *
+ * As a function, making the Ajax call is left up to yourself allowing
+ * complete control of the Ajax request. Indeed, if desired, a method other
+ * than Ajax could be used to obtain the required data, such as Web storage
+ * or an AIR database.
+ *
+ * The function is given four parameters and no return is required. The
+ * parameters are:
+ *
+ * 1. _object_ - Data to send to the server
+ * 2. _function_ - Callback function that must be executed when the required
+ * data has been obtained. That data should be passed into the callback
+ * as the only parameter
+ * 3. _object_ - DataTables settings object for the table
+ *
+ * Note that this supersedes `fnServerData` from DataTables 1.9-.
+ *
+ * @type string|object|function
+ * @default null
+ *
+ * @dtopt Option
+ * @name DataTable.defaults.ajax
+ * @since 1.10.0
+ *
+ * @example
+ * // Get JSON data from a file via Ajax.
+ * // Note DataTables expects data in the form `{ data: [ ...data... ] }` by default).
+ * $('#example').dataTable( {
+ * "ajax": "data.json"
+ * } );
+ *
+ * @example
+ * // Get JSON data from a file via Ajax, using `dataSrc` to change
+ * // `data` to `tableData` (i.e. `{ tableData: [ ...data... ] }`)
+ * $('#example').dataTable( {
+ * "ajax": {
+ * "url": "data.json",
+ * "dataSrc": "tableData"
+ * }
+ * } );
+ *
+ * @example
+ * // Get JSON data from a file via Ajax, using `dataSrc` to read data
+ * // from a plain array rather than an array in an object
+ * $('#example').dataTable( {
+ * "ajax": {
+ * "url": "data.json",
+ * "dataSrc": ""
+ * }
+ * } );
+ *
+ * @example
+ * // Manipulate the data returned from the server - add a link to data
+ * // (note this can, should, be done using `render` for the column - this
+ * // is just a simple example of how the data can be manipulated).
+ * $('#example').dataTable( {
+ * "ajax": {
+ * "url": "data.json",
+ * "dataSrc": function ( json ) {
+ * for ( var i=0, ien=json.length ; i
+ * a string - class name will be matched on the TH for the column
+ * 0 or a positive integer - column index counting from the left
+ * a negative integer - column index counting from the right
+ * the string "_all" - all columns (i.e. assign a default)
+ *
+ * @member
+ *
+ * @name DataTable.defaults.columnDefs
+ */
+ "aoColumnDefs": null,
+
+
+ /**
+ * Basically the same as `search`, this parameter defines the individual column
+ * filtering state at initialisation time. The array must be of the same size
+ * as the number of columns, and each element be an object with the parameters
+ * `search` and `escapeRegex` (the latter is optional). 'null' is also
+ * accepted and the default will be used.
+ * @type array
+ * @default []
+ *
+ * @dtopt Option
+ * @name DataTable.defaults.searchCols
+ *
+ * @example
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "searchCols": [
+ * null,
+ * { "search": "My filter" },
+ * null,
+ * { "search": "^[0-9]", "escapeRegex": false }
+ * ]
+ * } );
+ * } )
+ */
+ "aoSearchCols": [],
+
+
+ /**
+ * An array of CSS classes that should be applied to displayed rows. This
+ * array may be of any length, and DataTables will apply each class
+ * sequentially, looping when required.
+ * @type array
+ * @default null Will take the values determined by the `oClasses.stripe*`
+ * options
+ *
+ * @dtopt Option
+ * @name DataTable.defaults.stripeClasses
+ *
+ * @example
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "stripeClasses": [ 'strip1', 'strip2', 'strip3' ]
+ * } );
+ * } )
+ */
+ "asStripeClasses": null,
+
+
+ /**
+ * Enable or disable automatic column width calculation. This can be disabled
+ * as an optimisation (it takes some time to calculate the widths) if the
+ * tables widths are passed in using `columns`.
+ * @type boolean
+ * @default true
+ *
+ * @dtopt Features
+ * @name DataTable.defaults.autoWidth
+ *
+ * @example
+ * $(document).ready( function () {
+ * $('#example').dataTable( {
+ * "autoWidth": false
+ * } );
+ * } );
+ */
+ "bAutoWidth": true,
+
+
+ /**
+ * Deferred rendering can provide DataTables with a huge speed boost when you
+ * are using an Ajax or JS data source for the table. This option, when set to
+ * true, will cause DataTables to defer the creation of the table elements for
+ * each row until they are needed for a draw - saving a significant amount of
+ * time.
+ * @type boolean
+ * @default false
+ *
+ * @dtopt Features
+ * @name DataTable.defaults.deferRender
+ *
+ * @example
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "ajax": "sources/arrays.txt",
+ * "deferRender": true
+ * } );
+ * } );
+ */
+ "bDeferRender": false,
+
+
+ /**
+ * Replace a DataTable which matches the given selector and replace it with
+ * one which has the properties of the new initialisation object passed. If no
+ * table matches the selector, then the new DataTable will be constructed as
+ * per normal.
+ * @type boolean
+ * @default false
+ *
+ * @dtopt Options
+ * @name DataTable.defaults.destroy
+ *
+ * @example
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "srollY": "200px",
+ * "paginate": false
+ * } );
+ *
+ * // Some time later....
+ * $('#example').dataTable( {
+ * "filter": false,
+ * "destroy": true
+ * } );
+ * } );
+ */
+ "bDestroy": false,
+
+
+ /**
+ * Enable or disable filtering of data. Filtering in DataTables is "smart" in
+ * that it allows the end user to input multiple words (space separated) and
+ * will match a row containing those words, even if not in the order that was
+ * specified (this allow matching across multiple columns). Note that if you
+ * wish to use filtering in DataTables this must remain 'true' - to remove the
+ * default filtering input box and retain filtering abilities, please use
+ * {@link DataTable.defaults.dom}.
+ * @type boolean
+ * @default true
+ *
+ * @dtopt Features
+ * @name DataTable.defaults.searching
+ *
+ * @example
+ * $(document).ready( function () {
+ * $('#example').dataTable( {
+ * "searching": false
+ * } );
+ * } );
+ */
+ "bFilter": true,
+
+
+ /**
+ * Enable or disable the table information display. This shows information
+ * about the data that is currently visible on the page, including information
+ * about filtered data if that action is being performed.
+ * @type boolean
+ * @default true
+ *
+ * @dtopt Features
+ * @name DataTable.defaults.info
+ *
+ * @example
+ * $(document).ready( function () {
+ * $('#example').dataTable( {
+ * "info": false
+ * } );
+ * } );
+ */
+ "bInfo": true,
+
+
+ /**
+ * Allows the end user to select the size of a formatted page from a select
+ * menu (sizes are 10, 25, 50 and 100). Requires pagination (`paginate`).
+ * @type boolean
+ * @default true
+ *
+ * @dtopt Features
+ * @name DataTable.defaults.lengthChange
+ *
+ * @example
+ * $(document).ready( function () {
+ * $('#example').dataTable( {
+ * "lengthChange": false
+ * } );
+ * } );
+ */
+ "bLengthChange": true,
+
+
+ /**
+ * Enable or disable pagination.
+ * @type boolean
+ * @default true
+ *
+ * @dtopt Features
+ * @name DataTable.defaults.paging
+ *
+ * @example
+ * $(document).ready( function () {
+ * $('#example').dataTable( {
+ * "paging": false
+ * } );
+ * } );
+ */
+ "bPaginate": true,
+
+
+ /**
+ * Enable or disable the display of a 'processing' indicator when the table is
+ * being processed (e.g. a sort). This is particularly useful for tables with
+ * large amounts of data where it can take a noticeable amount of time to sort
+ * the entries.
+ * @type boolean
+ * @default false
+ *
+ * @dtopt Features
+ * @name DataTable.defaults.processing
+ *
+ * @example
+ * $(document).ready( function () {
+ * $('#example').dataTable( {
+ * "processing": true
+ * } );
+ * } );
+ */
+ "bProcessing": false,
+
+
+ /**
+ * Retrieve the DataTables object for the given selector. Note that if the
+ * table has already been initialised, this parameter will cause DataTables
+ * to simply return the object that has already been set up - it will not take
+ * account of any changes you might have made to the initialisation object
+ * passed to DataTables (setting this parameter to true is an acknowledgement
+ * that you understand this). `destroy` can be used to reinitialise a table if
+ * you need.
+ * @type boolean
+ * @default false
+ *
+ * @dtopt Options
+ * @name DataTable.defaults.retrieve
+ *
+ * @example
+ * $(document).ready( function() {
+ * initTable();
+ * tableActions();
+ * } );
+ *
+ * function initTable ()
+ * {
+ * return $('#example').dataTable( {
+ * "scrollY": "200px",
+ * "paginate": false,
+ * "retrieve": true
+ * } );
+ * }
+ *
+ * function tableActions ()
+ * {
+ * var table = initTable();
+ * // perform API operations with oTable
+ * }
+ */
+ "bRetrieve": false,
+
+
+ /**
+ * When vertical (y) scrolling is enabled, DataTables will force the height of
+ * the table's viewport to the given height at all times (useful for layout).
+ * However, this can look odd when filtering data down to a small data set,
+ * and the footer is left "floating" further down. This parameter (when
+ * enabled) will cause DataTables to collapse the table's viewport down when
+ * the result set will fit within the given Y height.
+ * @type boolean
+ * @default false
+ *
+ * @dtopt Options
+ * @name DataTable.defaults.scrollCollapse
+ *
+ * @example
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "scrollY": "200",
+ * "scrollCollapse": true
+ * } );
+ * } );
+ */
+ "bScrollCollapse": false,
+
+
+ /**
+ * Configure DataTables to use server-side processing. Note that the
+ * `ajax` parameter must also be given in order to give DataTables a
+ * source to obtain the required data for each draw.
+ * @type boolean
+ * @default false
+ *
+ * @dtopt Features
+ * @dtopt Server-side
+ * @name DataTable.defaults.serverSide
+ *
+ * @example
+ * $(document).ready( function () {
+ * $('#example').dataTable( {
+ * "serverSide": true,
+ * "ajax": "xhr.php"
+ * } );
+ * } );
+ */
+ "bServerSide": false,
+
+
+ /**
+ * Enable or disable sorting of columns. Sorting of individual columns can be
+ * disabled by the `sortable` option for each column.
+ * @type boolean
+ * @default true
+ *
+ * @dtopt Features
+ * @name DataTable.defaults.ordering
+ *
+ * @example
+ * $(document).ready( function () {
+ * $('#example').dataTable( {
+ * "ordering": false
+ * } );
+ * } );
+ */
+ "bSort": true,
+
+
+ /**
+ * Enable or display DataTables' ability to sort multiple columns at the
+ * same time (activated by shift-click by the user).
+ * @type boolean
+ * @default true
+ *
+ * @dtopt Options
+ * @name DataTable.defaults.orderMulti
+ *
+ * @example
+ * // Disable multiple column sorting ability
+ * $(document).ready( function () {
+ * $('#example').dataTable( {
+ * "orderMulti": false
+ * } );
+ * } );
+ */
+ "bSortMulti": true,
+
+
+ /**
+ * Allows control over whether DataTables should use the top (true) unique
+ * cell that is found for a single column, or the bottom (false - default).
+ * This is useful when using complex headers.
+ * @type boolean
+ * @default false
+ *
+ * @dtopt Options
+ * @name DataTable.defaults.orderCellsTop
+ *
+ * @example
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "orderCellsTop": true
+ * } );
+ * } );
+ */
+ "bSortCellsTop": false,
+
+
+ /**
+ * Enable or disable the addition of the classes `sorting\_1`, `sorting\_2` and
+ * `sorting\_3` to the columns which are currently being sorted on. This is
+ * presented as a feature switch as it can increase processing time (while
+ * classes are removed and added) so for large data sets you might want to
+ * turn this off.
+ * @type boolean
+ * @default true
+ *
+ * @dtopt Features
+ * @name DataTable.defaults.orderClasses
+ *
+ * @example
+ * $(document).ready( function () {
+ * $('#example').dataTable( {
+ * "orderClasses": false
+ * } );
+ * } );
+ */
+ "bSortClasses": true,
+
+
+ /**
+ * Enable or disable state saving. When enabled HTML5 `localStorage` will be
+ * used to save table display information such as pagination information,
+ * display length, filtering and sorting. As such when the end user reloads
+ * the page the display display will match what thy had previously set up.
+ *
+ * Due to the use of `localStorage` the default state saving is not supported
+ * in IE6 or 7. If state saving is required in those browsers, use
+ * `stateSaveCallback` to provide a storage solution such as cookies.
+ * @type boolean
+ * @default false
+ *
+ * @dtopt Features
+ * @name DataTable.defaults.stateSave
+ *
+ * @example
+ * $(document).ready( function () {
+ * $('#example').dataTable( {
+ * "stateSave": true
+ * } );
+ * } );
+ */
+ "bStateSave": false,
+
+
+ /**
+ * This function is called when a TR element is created (and all TD child
+ * elements have been inserted), or registered if using a DOM source, allowing
+ * manipulation of the TR element (adding classes etc).
+ * @type function
+ * @param {node} row "TR" element for the current row
+ * @param {array} data Raw data array for this row
+ * @param {int} dataIndex The index of this row in the internal aoData array
+ *
+ * @dtopt Callbacks
+ * @name DataTable.defaults.createdRow
+ *
+ * @example
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "createdRow": function( row, data, dataIndex ) {
+ * // Bold the grade for all 'A' grade browsers
+ * if ( data[4] == "A" )
+ * {
+ * $('td:eq(4)', row).html( 'A ' );
+ * }
+ * }
+ * } );
+ * } );
+ */
+ "fnCreatedRow": null,
+
+
+ /**
+ * This function is called on every 'draw' event, and allows you to
+ * dynamically modify any aspect you want about the created DOM.
+ * @type function
+ * @param {object} settings DataTables settings object
+ *
+ * @dtopt Callbacks
+ * @name DataTable.defaults.drawCallback
+ *
+ * @example
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "drawCallback": function( settings ) {
+ * alert( 'DataTables has redrawn the table' );
+ * }
+ * } );
+ * } );
+ */
+ "fnDrawCallback": null,
+
+
+ /**
+ * Identical to fnHeaderCallback() but for the table footer this function
+ * allows you to modify the table footer on every 'draw' event.
+ * @type function
+ * @param {node} foot "TR" element for the footer
+ * @param {array} data Full table data (as derived from the original HTML)
+ * @param {int} start Index for the current display starting point in the
+ * display array
+ * @param {int} end Index for the current display ending point in the
+ * display array
+ * @param {array int} display Index array to translate the visual position
+ * to the full data array
+ *
+ * @dtopt Callbacks
+ * @name DataTable.defaults.footerCallback
+ *
+ * @example
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "footerCallback": function( tfoot, data, start, end, display ) {
+ * tfoot.getElementsByTagName('th')[0].innerHTML = "Starting index is "+start;
+ * }
+ * } );
+ * } )
+ */
+ "fnFooterCallback": null,
+
+
+ /**
+ * When rendering large numbers in the information element for the table
+ * (i.e. "Showing 1 to 10 of 57 entries") DataTables will render large numbers
+ * to have a comma separator for the 'thousands' units (e.g. 1 million is
+ * rendered as "1,000,000") to help readability for the end user. This
+ * function will override the default method DataTables uses.
+ * @type function
+ * @member
+ * @param {int} toFormat number to be formatted
+ * @returns {string} formatted string for DataTables to show the number
+ *
+ * @dtopt Callbacks
+ * @name DataTable.defaults.formatNumber
+ *
+ * @example
+ * // Format a number using a single quote for the separator (note that
+ * // this can also be done with the language.thousands option)
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "formatNumber": function ( toFormat ) {
+ * return toFormat.toString().replace(
+ * /\B(?=(\d{3})+(?!\d))/g, "'"
+ * );
+ * };
+ * } );
+ * } );
+ */
+ "fnFormatNumber": function ( toFormat ) {
+ return toFormat.toString().replace(
+ /\B(?=(\d{3})+(?!\d))/g,
+ this.oLanguage.sThousands
+ );
+ },
+
+
+ /**
+ * This function is called on every 'draw' event, and allows you to
+ * dynamically modify the header row. This can be used to calculate and
+ * display useful information about the table.
+ * @type function
+ * @param {node} head "TR" element for the header
+ * @param {array} data Full table data (as derived from the original HTML)
+ * @param {int} start Index for the current display starting point in the
+ * display array
+ * @param {int} end Index for the current display ending point in the
+ * display array
+ * @param {array int} display Index array to translate the visual position
+ * to the full data array
+ *
+ * @dtopt Callbacks
+ * @name DataTable.defaults.headerCallback
+ *
+ * @example
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "fheaderCallback": function( head, data, start, end, display ) {
+ * head.getElementsByTagName('th')[0].innerHTML = "Displaying "+(end-start)+" records";
+ * }
+ * } );
+ * } )
+ */
+ "fnHeaderCallback": null,
+
+
+ /**
+ * The information element can be used to convey information about the current
+ * state of the table. Although the internationalisation options presented by
+ * DataTables are quite capable of dealing with most customisations, there may
+ * be times where you wish to customise the string further. This callback
+ * allows you to do exactly that.
+ * @type function
+ * @param {object} oSettings DataTables settings object
+ * @param {int} start Starting position in data for the draw
+ * @param {int} end End position in data for the draw
+ * @param {int} max Total number of rows in the table (regardless of
+ * filtering)
+ * @param {int} total Total number of rows in the data set, after filtering
+ * @param {string} pre The string that DataTables has formatted using it's
+ * own rules
+ * @returns {string} The string to be displayed in the information element.
+ *
+ * @dtopt Callbacks
+ * @name DataTable.defaults.infoCallback
+ *
+ * @example
+ * $('#example').dataTable( {
+ * "infoCallback": function( settings, start, end, max, total, pre ) {
+ * return start +" to "+ end;
+ * }
+ * } );
+ */
+ "fnInfoCallback": null,
+
+
+ /**
+ * Called when the table has been initialised. Normally DataTables will
+ * initialise sequentially and there will be no need for this function,
+ * however, this does not hold true when using external language information
+ * since that is obtained using an async XHR call.
+ * @type function
+ * @param {object} settings DataTables settings object
+ * @param {object} json The JSON object request from the server - only
+ * present if client-side Ajax sourced data is used
+ *
+ * @dtopt Callbacks
+ * @name DataTable.defaults.initComplete
+ *
+ * @example
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "initComplete": function(settings, json) {
+ * alert( 'DataTables has finished its initialisation.' );
+ * }
+ * } );
+ * } )
+ */
+ "fnInitComplete": null,
+
+
+ /**
+ * Called at the very start of each table draw and can be used to cancel the
+ * draw by returning false, any other return (including undefined) results in
+ * the full draw occurring).
+ * @type function
+ * @param {object} settings DataTables settings object
+ * @returns {boolean} False will cancel the draw, anything else (including no
+ * return) will allow it to complete.
+ *
+ * @dtopt Callbacks
+ * @name DataTable.defaults.preDrawCallback
+ *
+ * @example
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "preDrawCallback": function( settings ) {
+ * if ( $('#test').val() == 1 ) {
+ * return false;
+ * }
+ * }
+ * } );
+ * } );
+ */
+ "fnPreDrawCallback": null,
+
+
+ /**
+ * This function allows you to 'post process' each row after it have been
+ * generated for each table draw, but before it is rendered on screen. This
+ * function might be used for setting the row class name etc.
+ * @type function
+ * @param {node} row "TR" element for the current row
+ * @param {array} data Raw data array for this row
+ * @param {int} displayIndex The display index for the current table draw
+ * @param {int} displayIndexFull The index of the data in the full list of
+ * rows (after filtering)
+ *
+ * @dtopt Callbacks
+ * @name DataTable.defaults.rowCallback
+ *
+ * @example
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "rowCallback": function( row, data, displayIndex, displayIndexFull ) {
+ * // Bold the grade for all 'A' grade browsers
+ * if ( data[4] == "A" ) {
+ * $('td:eq(4)', row).html( 'A ' );
+ * }
+ * }
+ * } );
+ * } );
+ */
+ "fnRowCallback": null,
+
+
+ /**
+ * __Deprecated__ The functionality provided by this parameter has now been
+ * superseded by that provided through `ajax`, which should be used instead.
+ *
+ * This parameter allows you to override the default function which obtains
+ * the data from the server so something more suitable for your application.
+ * For example you could use POST data, or pull information from a Gears or
+ * AIR database.
+ * @type function
+ * @member
+ * @param {string} source HTTP source to obtain the data from (`ajax`)
+ * @param {array} data A key/value pair object containing the data to send
+ * to the server
+ * @param {function} callback to be called on completion of the data get
+ * process that will draw the data on the page.
+ * @param {object} settings DataTables settings object
+ *
+ * @dtopt Callbacks
+ * @dtopt Server-side
+ * @name DataTable.defaults.serverData
+ *
+ * @deprecated 1.10. Please use `ajax` for this functionality now.
+ */
+ "fnServerData": null,
+
+
+ /**
+ * __Deprecated__ The functionality provided by this parameter has now been
+ * superseded by that provided through `ajax`, which should be used instead.
+ *
+ * It is often useful to send extra data to the server when making an Ajax
+ * request - for example custom filtering information, and this callback
+ * function makes it trivial to send extra information to the server. The
+ * passed in parameter is the data set that has been constructed by
+ * DataTables, and you can add to this or modify it as you require.
+ * @type function
+ * @param {array} data Data array (array of objects which are name/value
+ * pairs) that has been constructed by DataTables and will be sent to the
+ * server. In the case of Ajax sourced data with server-side processing
+ * this will be an empty array, for server-side processing there will be a
+ * significant number of parameters!
+ * @returns {undefined} Ensure that you modify the data array passed in,
+ * as this is passed by reference.
+ *
+ * @dtopt Callbacks
+ * @dtopt Server-side
+ * @name DataTable.defaults.serverParams
+ *
+ * @deprecated 1.10. Please use `ajax` for this functionality now.
+ */
+ "fnServerParams": null,
+
+
+ /**
+ * Load the table state. With this function you can define from where, and how, the
+ * state of a table is loaded. By default DataTables will load from `localStorage`
+ * but you might wish to use a server-side database or cookies.
+ * @type function
+ * @member
+ * @param {object} settings DataTables settings object
+ * @param {object} callback Callback that can be executed when done. It
+ * should be passed the loaded state object.
+ * @return {object} The DataTables state object to be loaded
+ *
+ * @dtopt Callbacks
+ * @name DataTable.defaults.stateLoadCallback
+ *
+ * @example
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "stateSave": true,
+ * "stateLoadCallback": function (settings, callback) {
+ * $.ajax( {
+ * "url": "/state_load",
+ * "dataType": "json",
+ * "success": function (json) {
+ * callback( json );
+ * }
+ * } );
+ * }
+ * } );
+ * } );
+ */
+ "fnStateLoadCallback": function ( settings ) {
+ try {
+ return JSON.parse(
+ (settings.iStateDuration === -1 ? sessionStorage : localStorage).getItem(
+ 'DataTables_'+settings.sInstance+'_'+location.pathname
+ )
+ );
+ } catch (e) {}
+ },
+
+
+ /**
+ * Callback which allows modification of the saved state prior to loading that state.
+ * This callback is called when the table is loading state from the stored data, but
+ * prior to the settings object being modified by the saved state. Note that for
+ * plug-in authors, you should use the `stateLoadParams` event to load parameters for
+ * a plug-in.
+ * @type function
+ * @param {object} settings DataTables settings object
+ * @param {object} data The state object that is to be loaded
+ *
+ * @dtopt Callbacks
+ * @name DataTable.defaults.stateLoadParams
+ *
+ * @example
+ * // Remove a saved filter, so filtering is never loaded
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "stateSave": true,
+ * "stateLoadParams": function (settings, data) {
+ * data.oSearch.sSearch = "";
+ * }
+ * } );
+ * } );
+ *
+ * @example
+ * // Disallow state loading by returning false
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "stateSave": true,
+ * "stateLoadParams": function (settings, data) {
+ * return false;
+ * }
+ * } );
+ * } );
+ */
+ "fnStateLoadParams": null,
+
+
+ /**
+ * Callback that is called when the state has been loaded from the state saving method
+ * and the DataTables settings object has been modified as a result of the loaded state.
+ * @type function
+ * @param {object} settings DataTables settings object
+ * @param {object} data The state object that was loaded
+ *
+ * @dtopt Callbacks
+ * @name DataTable.defaults.stateLoaded
+ *
+ * @example
+ * // Show an alert with the filtering value that was saved
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "stateSave": true,
+ * "stateLoaded": function (settings, data) {
+ * alert( 'Saved filter was: '+data.oSearch.sSearch );
+ * }
+ * } );
+ * } );
+ */
+ "fnStateLoaded": null,
+
+
+ /**
+ * Save the table state. This function allows you to define where and how the state
+ * information for the table is stored By default DataTables will use `localStorage`
+ * but you might wish to use a server-side database or cookies.
+ * @type function
+ * @member
+ * @param {object} settings DataTables settings object
+ * @param {object} data The state object to be saved
+ *
+ * @dtopt Callbacks
+ * @name DataTable.defaults.stateSaveCallback
+ *
+ * @example
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "stateSave": true,
+ * "stateSaveCallback": function (settings, data) {
+ * // Send an Ajax request to the server with the state object
+ * $.ajax( {
+ * "url": "/state_save",
+ * "data": data,
+ * "dataType": "json",
+ * "method": "POST"
+ * "success": function () {}
+ * } );
+ * }
+ * } );
+ * } );
+ */
+ "fnStateSaveCallback": function ( settings, data ) {
+ try {
+ (settings.iStateDuration === -1 ? sessionStorage : localStorage).setItem(
+ 'DataTables_'+settings.sInstance+'_'+location.pathname,
+ JSON.stringify( data )
+ );
+ } catch (e) {}
+ },
+
+
+ /**
+ * Callback which allows modification of the state to be saved. Called when the table
+ * has changed state a new state save is required. This method allows modification of
+ * the state saving object prior to actually doing the save, including addition or
+ * other state properties or modification. Note that for plug-in authors, you should
+ * use the `stateSaveParams` event to save parameters for a plug-in.
+ * @type function
+ * @param {object} settings DataTables settings object
+ * @param {object} data The state object to be saved
+ *
+ * @dtopt Callbacks
+ * @name DataTable.defaults.stateSaveParams
+ *
+ * @example
+ * // Remove a saved filter, so filtering is never saved
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "stateSave": true,
+ * "stateSaveParams": function (settings, data) {
+ * data.oSearch.sSearch = "";
+ * }
+ * } );
+ * } );
+ */
+ "fnStateSaveParams": null,
+
+
+ /**
+ * Duration for which the saved state information is considered valid. After this period
+ * has elapsed the state will be returned to the default.
+ * Value is given in seconds.
+ * @type int
+ * @default 7200 (2 hours)
+ *
+ * @dtopt Options
+ * @name DataTable.defaults.stateDuration
+ *
+ * @example
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "stateDuration": 60*60*24; // 1 day
+ * } );
+ * } )
+ */
+ "iStateDuration": 7200,
+
+
+ /**
+ * When enabled DataTables will not make a request to the server for the first
+ * page draw - rather it will use the data already on the page (no sorting etc
+ * will be applied to it), thus saving on an XHR at load time. `deferLoading`
+ * is used to indicate that deferred loading is required, but it is also used
+ * to tell DataTables how many records there are in the full table (allowing
+ * the information element and pagination to be displayed correctly). In the case
+ * where a filtering is applied to the table on initial load, this can be
+ * indicated by giving the parameter as an array, where the first element is
+ * the number of records available after filtering and the second element is the
+ * number of records without filtering (allowing the table information element
+ * to be shown correctly).
+ * @type int | array
+ * @default null
+ *
+ * @dtopt Options
+ * @name DataTable.defaults.deferLoading
+ *
+ * @example
+ * // 57 records available in the table, no filtering applied
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "serverSide": true,
+ * "ajax": "scripts/server_processing.php",
+ * "deferLoading": 57
+ * } );
+ * } );
+ *
+ * @example
+ * // 57 records after filtering, 100 without filtering (an initial filter applied)
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "serverSide": true,
+ * "ajax": "scripts/server_processing.php",
+ * "deferLoading": [ 57, 100 ],
+ * "search": {
+ * "search": "my_filter"
+ * }
+ * } );
+ * } );
+ */
+ "iDeferLoading": null,
+
+
+ /**
+ * Number of rows to display on a single page when using pagination. If
+ * feature enabled (`lengthChange`) then the end user will be able to override
+ * this to a custom setting using a pop-up menu.
+ * @type int
+ * @default 10
+ *
+ * @dtopt Options
+ * @name DataTable.defaults.pageLength
+ *
+ * @example
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "pageLength": 50
+ * } );
+ * } )
+ */
+ "iDisplayLength": 10,
+
+
+ /**
+ * Define the starting point for data display when using DataTables with
+ * pagination. Note that this parameter is the number of records, rather than
+ * the page number, so if you have 10 records per page and want to start on
+ * the third page, it should be "20".
+ * @type int
+ * @default 0
+ *
+ * @dtopt Options
+ * @name DataTable.defaults.displayStart
+ *
+ * @example
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "displayStart": 20
+ * } );
+ * } )
+ */
+ "iDisplayStart": 0,
+
+
+ /**
+ * By default DataTables allows keyboard navigation of the table (sorting, paging,
+ * and filtering) by adding a `tabindex` attribute to the required elements. This
+ * allows you to tab through the controls and press the enter key to activate them.
+ * The tabindex is default 0, meaning that the tab follows the flow of the document.
+ * You can overrule this using this parameter if you wish. Use a value of -1 to
+ * disable built-in keyboard navigation.
+ * @type int
+ * @default 0
+ *
+ * @dtopt Options
+ * @name DataTable.defaults.tabIndex
+ *
+ * @example
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "tabIndex": 1
+ * } );
+ * } );
+ */
+ "iTabIndex": 0,
+
+
+ /**
+ * Classes that DataTables assigns to the various components and features
+ * that it adds to the HTML table. This allows classes to be configured
+ * during initialisation in addition to through the static
+ * {@link DataTable.ext.oStdClasses} object).
+ * @namespace
+ * @name DataTable.defaults.classes
+ */
+ "oClasses": {},
+
+
+ /**
+ * All strings that DataTables uses in the user interface that it creates
+ * are defined in this object, allowing you to modified them individually or
+ * completely replace them all as required.
+ * @namespace
+ * @name DataTable.defaults.language
+ */
+ "oLanguage": {
+ /**
+ * Strings that are used for WAI-ARIA labels and controls only (these are not
+ * actually visible on the page, but will be read by screenreaders, and thus
+ * must be internationalised as well).
+ * @namespace
+ * @name DataTable.defaults.language.aria
+ */
+ "oAria": {
+ /**
+ * ARIA label that is added to the table headers when the column may be
+ * sorted ascending by activing the column (click or return when focused).
+ * Note that the column header is prefixed to this string.
+ * @type string
+ * @default : activate to sort column ascending
+ *
+ * @dtopt Language
+ * @name DataTable.defaults.language.aria.sortAscending
+ *
+ * @example
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "language": {
+ * "aria": {
+ * "sortAscending": " - click/return to sort ascending"
+ * }
+ * }
+ * } );
+ * } );
+ */
+ "sSortAscending": ": activate to sort column ascending",
+
+ /**
+ * ARIA label that is added to the table headers when the column may be
+ * sorted descending by activing the column (click or return when focused).
+ * Note that the column header is prefixed to this string.
+ * @type string
+ * @default : activate to sort column ascending
+ *
+ * @dtopt Language
+ * @name DataTable.defaults.language.aria.sortDescending
+ *
+ * @example
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "language": {
+ * "aria": {
+ * "sortDescending": " - click/return to sort descending"
+ * }
+ * }
+ * } );
+ * } );
+ */
+ "sSortDescending": ": activate to sort column descending"
+ },
+
+ /**
+ * Pagination string used by DataTables for the built-in pagination
+ * control types.
+ * @namespace
+ * @name DataTable.defaults.language.paginate
+ */
+ "oPaginate": {
+ /**
+ * Text to use when using the 'full_numbers' type of pagination for the
+ * button to take the user to the first page.
+ * @type string
+ * @default First
+ *
+ * @dtopt Language
+ * @name DataTable.defaults.language.paginate.first
+ *
+ * @example
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "language": {
+ * "paginate": {
+ * "first": "First page"
+ * }
+ * }
+ * } );
+ * } );
+ */
+ "sFirst": "First",
+
+
+ /**
+ * Text to use when using the 'full_numbers' type of pagination for the
+ * button to take the user to the last page.
+ * @type string
+ * @default Last
+ *
+ * @dtopt Language
+ * @name DataTable.defaults.language.paginate.last
+ *
+ * @example
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "language": {
+ * "paginate": {
+ * "last": "Last page"
+ * }
+ * }
+ * } );
+ * } );
+ */
+ "sLast": "Last",
+
+
+ /**
+ * Text to use for the 'next' pagination button (to take the user to the
+ * next page).
+ * @type string
+ * @default Next
+ *
+ * @dtopt Language
+ * @name DataTable.defaults.language.paginate.next
+ *
+ * @example
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "language": {
+ * "paginate": {
+ * "next": "Next page"
+ * }
+ * }
+ * } );
+ * } );
+ */
+ "sNext": "Next",
+
+
+ /**
+ * Text to use for the 'previous' pagination button (to take the user to
+ * the previous page).
+ * @type string
+ * @default Previous
+ *
+ * @dtopt Language
+ * @name DataTable.defaults.language.paginate.previous
+ *
+ * @example
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "language": {
+ * "paginate": {
+ * "previous": "Previous page"
+ * }
+ * }
+ * } );
+ * } );
+ */
+ "sPrevious": "Previous"
+ },
+
+ /**
+ * This string is shown in preference to `zeroRecords` when the table is
+ * empty of data (regardless of filtering). Note that this is an optional
+ * parameter - if it is not given, the value of `zeroRecords` will be used
+ * instead (either the default or given value).
+ * @type string
+ * @default No data available in table
+ *
+ * @dtopt Language
+ * @name DataTable.defaults.language.emptyTable
+ *
+ * @example
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "language": {
+ * "emptyTable": "No data available in table"
+ * }
+ * } );
+ * } );
+ */
+ "sEmptyTable": "No data available in table",
+
+
+ /**
+ * This string gives information to the end user about the information
+ * that is current on display on the page. The following tokens can be
+ * used in the string and will be dynamically replaced as the table
+ * display updates. This tokens can be placed anywhere in the string, or
+ * removed as needed by the language requires:
+ *
+ * * `\_START\_` - Display index of the first record on the current page
+ * * `\_END\_` - Display index of the last record on the current page
+ * * `\_TOTAL\_` - Number of records in the table after filtering
+ * * `\_MAX\_` - Number of records in the table without filtering
+ * * `\_PAGE\_` - Current page number
+ * * `\_PAGES\_` - Total number of pages of data in the table
+ *
+ * @type string
+ * @default Showing _START_ to _END_ of _TOTAL_ entries
+ *
+ * @dtopt Language
+ * @name DataTable.defaults.language.info
+ *
+ * @example
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "language": {
+ * "info": "Showing page _PAGE_ of _PAGES_"
+ * }
+ * } );
+ * } );
+ */
+ "sInfo": "Showing _START_ to _END_ of _TOTAL_ entries",
+
+
+ /**
+ * Display information string for when the table is empty. Typically the
+ * format of this string should match `info`.
+ * @type string
+ * @default Showing 0 to 0 of 0 entries
+ *
+ * @dtopt Language
+ * @name DataTable.defaults.language.infoEmpty
+ *
+ * @example
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "language": {
+ * "infoEmpty": "No entries to show"
+ * }
+ * } );
+ * } );
+ */
+ "sInfoEmpty": "Showing 0 to 0 of 0 entries",
+
+
+ /**
+ * When a user filters the information in a table, this string is appended
+ * to the information (`info`) to give an idea of how strong the filtering
+ * is. The variable _MAX_ is dynamically updated.
+ * @type string
+ * @default (filtered from _MAX_ total entries)
+ *
+ * @dtopt Language
+ * @name DataTable.defaults.language.infoFiltered
+ *
+ * @example
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "language": {
+ * "infoFiltered": " - filtering from _MAX_ records"
+ * }
+ * } );
+ * } );
+ */
+ "sInfoFiltered": "(filtered from _MAX_ total entries)",
+
+
+ /**
+ * If can be useful to append extra information to the info string at times,
+ * and this variable does exactly that. This information will be appended to
+ * the `info` (`infoEmpty` and `infoFiltered` in whatever combination they are
+ * being used) at all times.
+ * @type string
+ * @default Empty string
+ *
+ * @dtopt Language
+ * @name DataTable.defaults.language.infoPostFix
+ *
+ * @example
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "language": {
+ * "infoPostFix": "All records shown are derived from real information."
+ * }
+ * } );
+ * } );
+ */
+ "sInfoPostFix": "",
+
+
+ /**
+ * This decimal place operator is a little different from the other
+ * language options since DataTables doesn't output floating point
+ * numbers, so it won't ever use this for display of a number. Rather,
+ * what this parameter does is modify the sort methods of the table so
+ * that numbers which are in a format which has a character other than
+ * a period (`.`) as a decimal place will be sorted numerically.
+ *
+ * Note that numbers with different decimal places cannot be shown in
+ * the same table and still be sortable, the table must be consistent.
+ * However, multiple different tables on the page can use different
+ * decimal place characters.
+ * @type string
+ * @default
+ *
+ * @dtopt Language
+ * @name DataTable.defaults.language.decimal
+ *
+ * @example
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "language": {
+ * "decimal": ","
+ * "thousands": "."
+ * }
+ * } );
+ * } );
+ */
+ "sDecimal": "",
+
+
+ /**
+ * DataTables has a build in number formatter (`formatNumber`) which is
+ * used to format large numbers that are used in the table information.
+ * By default a comma is used, but this can be trivially changed to any
+ * character you wish with this parameter.
+ * @type string
+ * @default ,
+ *
+ * @dtopt Language
+ * @name DataTable.defaults.language.thousands
+ *
+ * @example
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "language": {
+ * "thousands": "'"
+ * }
+ * } );
+ * } );
+ */
+ "sThousands": ",",
+
+
+ /**
+ * Detail the action that will be taken when the drop down menu for the
+ * pagination length option is changed. The '_MENU_' variable is replaced
+ * with a default select list of 10, 25, 50 and 100, and can be replaced
+ * with a custom select box if required.
+ * @type string
+ * @default Show _MENU_ entries
+ *
+ * @dtopt Language
+ * @name DataTable.defaults.language.lengthMenu
+ *
+ * @example
+ * // Language change only
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "language": {
+ * "lengthMenu": "Display _MENU_ records"
+ * }
+ * } );
+ * } );
+ *
+ * @example
+ * // Language and options change
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "language": {
+ * "lengthMenu": 'Display '+
+ * '10 '+
+ * '20 '+
+ * '30 '+
+ * '40 '+
+ * '50 '+
+ * 'All '+
+ * ' records'
+ * }
+ * } );
+ * } );
+ */
+ "sLengthMenu": "Show _MENU_ entries",
+
+
+ /**
+ * When using Ajax sourced data and during the first draw when DataTables is
+ * gathering the data, this message is shown in an empty row in the table to
+ * indicate to the end user the the data is being loaded. Note that this
+ * parameter is not used when loading data by server-side processing, just
+ * Ajax sourced data with client-side processing.
+ * @type string
+ * @default Loading...
+ *
+ * @dtopt Language
+ * @name DataTable.defaults.language.loadingRecords
+ *
+ * @example
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "language": {
+ * "loadingRecords": "Please wait - loading..."
+ * }
+ * } );
+ * } );
+ */
+ "sLoadingRecords": "Loading...",
+
+
+ /**
+ * Text which is displayed when the table is processing a user action
+ * (usually a sort command or similar).
+ * @type string
+ * @default Processing...
+ *
+ * @dtopt Language
+ * @name DataTable.defaults.language.processing
+ *
+ * @example
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "language": {
+ * "processing": "DataTables is currently busy"
+ * }
+ * } );
+ * } );
+ */
+ "sProcessing": "Processing...",
+
+
+ /**
+ * Details the actions that will be taken when the user types into the
+ * filtering input text box. The variable "_INPUT_", if used in the string,
+ * is replaced with the HTML text box for the filtering input allowing
+ * control over where it appears in the string. If "_INPUT_" is not given
+ * then the input box is appended to the string automatically.
+ * @type string
+ * @default Search:
+ *
+ * @dtopt Language
+ * @name DataTable.defaults.language.search
+ *
+ * @example
+ * // Input text box will be appended at the end automatically
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "language": {
+ * "search": "Filter records:"
+ * }
+ * } );
+ * } );
+ *
+ * @example
+ * // Specify where the filter should appear
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "language": {
+ * "search": "Apply filter _INPUT_ to table"
+ * }
+ * } );
+ * } );
+ */
+ "sSearch": "Search:",
+
+
+ /**
+ * Assign a `placeholder` attribute to the search `input` element
+ * @type string
+ * @default
+ *
+ * @dtopt Language
+ * @name DataTable.defaults.language.searchPlaceholder
+ */
+ "sSearchPlaceholder": "",
+
+
+ /**
+ * All of the language information can be stored in a file on the
+ * server-side, which DataTables will look up if this parameter is passed.
+ * It must store the URL of the language file, which is in a JSON format,
+ * and the object has the same properties as the oLanguage object in the
+ * initialiser object (i.e. the above parameters). Please refer to one of
+ * the example language files to see how this works in action.
+ * @type string
+ * @default Empty string - i.e. disabled
+ *
+ * @dtopt Language
+ * @name DataTable.defaults.language.url
+ *
+ * @example
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "language": {
+ * "url": "http://www.sprymedia.co.uk/dataTables/lang.txt"
+ * }
+ * } );
+ * } );
+ */
+ "sUrl": "",
+
+
+ /**
+ * Text shown inside the table records when the is no information to be
+ * displayed after filtering. `emptyTable` is shown when there is simply no
+ * information in the table at all (regardless of filtering).
+ * @type string
+ * @default No matching records found
+ *
+ * @dtopt Language
+ * @name DataTable.defaults.language.zeroRecords
+ *
+ * @example
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "language": {
+ * "zeroRecords": "No records to display"
+ * }
+ * } );
+ * } );
+ */
+ "sZeroRecords": "No matching records found"
+ },
+
+
+ /**
+ * This parameter allows you to have define the global filtering state at
+ * initialisation time. As an object the `search` parameter must be
+ * defined, but all other parameters are optional. When `regex` is true,
+ * the search string will be treated as a regular expression, when false
+ * (default) it will be treated as a straight string. When `smart`
+ * DataTables will use it's smart filtering methods (to word match at
+ * any point in the data), when false this will not be done.
+ * @namespace
+ * @extends DataTable.models.oSearch
+ *
+ * @dtopt Options
+ * @name DataTable.defaults.search
+ *
+ * @example
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "search": {"search": "Initial search"}
+ * } );
+ * } )
+ */
+ "oSearch": $.extend( {}, DataTable.models.oSearch ),
+
+
+ /**
+ * __Deprecated__ The functionality provided by this parameter has now been
+ * superseded by that provided through `ajax`, which should be used instead.
+ *
+ * By default DataTables will look for the property `data` (or `aaData` for
+ * compatibility with DataTables 1.9-) when obtaining data from an Ajax
+ * source or for server-side processing - this parameter allows that
+ * property to be changed. You can use Javascript dotted object notation to
+ * get a data source for multiple levels of nesting.
+ * @type string
+ * @default data
+ *
+ * @dtopt Options
+ * @dtopt Server-side
+ * @name DataTable.defaults.ajaxDataProp
+ *
+ * @deprecated 1.10. Please use `ajax` for this functionality now.
+ */
+ "sAjaxDataProp": "data",
+
+
+ /**
+ * __Deprecated__ The functionality provided by this parameter has now been
+ * superseded by that provided through `ajax`, which should be used instead.
+ *
+ * You can instruct DataTables to load data from an external
+ * source using this parameter (use aData if you want to pass data in you
+ * already have). Simply provide a url a JSON object can be obtained from.
+ * @type string
+ * @default null
+ *
+ * @dtopt Options
+ * @dtopt Server-side
+ * @name DataTable.defaults.ajaxSource
+ *
+ * @deprecated 1.10. Please use `ajax` for this functionality now.
+ */
+ "sAjaxSource": null,
+
+
+ /**
+ * This initialisation variable allows you to specify exactly where in the
+ * DOM you want DataTables to inject the various controls it adds to the page
+ * (for example you might want the pagination controls at the top of the
+ * table). DIV elements (with or without a custom class) can also be added to
+ * aid styling. The follow syntax is used:
+ *
+ * The following options are allowed:
+ *
+ * 'l' - Length changing
+ * 'f' - Filtering input
+ * 't' - The table!
+ * 'i' - Information
+ * 'p' - Pagination
+ * 'r' - pRocessing
+ *
+ *
+ * The following constants are allowed:
+ *
+ * 'H' - jQueryUI theme "header" classes ('fg-toolbar ui-widget-header ui-corner-tl ui-corner-tr ui-helper-clearfix')
+ * 'F' - jQueryUI theme "footer" classes ('fg-toolbar ui-widget-header ui-corner-bl ui-corner-br ui-helper-clearfix')
+ *
+ *
+ * The following syntax is expected:
+ *
+ * '<' and '>' - div elements
+ * '<"class" and '>' - div with a class
+ * '<"#id" and '>' - div with an ID
+ *
+ *
+ * Examples:
+ *
+ * '<"wrapper"flipt>'
+ * '<lf<t>ip>'
+ *
+ *
+ *
+ * @type string
+ * @default lfrtip (when `jQueryUI` is false) or
+ * <"H"lfr>t<"F"ip> (when `jQueryUI` is true)
+ *
+ * @dtopt Options
+ * @name DataTable.defaults.dom
+ *
+ * @example
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "dom": '<"top"i>rt<"bottom"flp><"clear">'
+ * } );
+ * } );
+ */
+ "sDom": "lfrtip",
+
+
+ /**
+ * Search delay option. This will throttle full table searches that use the
+ * DataTables provided search input element (it does not effect calls to
+ * `dt-api search()`, providing a delay before the search is made.
+ * @type integer
+ * @default 0
+ *
+ * @dtopt Options
+ * @name DataTable.defaults.searchDelay
+ *
+ * @example
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "searchDelay": 200
+ * } );
+ * } )
+ */
+ "searchDelay": null,
+
+
+ /**
+ * DataTables features six different built-in options for the buttons to
+ * display for pagination control:
+ *
+ * * `numbers` - Page number buttons only
+ * * `simple` - 'Previous' and 'Next' buttons only
+ * * 'simple_numbers` - 'Previous' and 'Next' buttons, plus page numbers
+ * * `full` - 'First', 'Previous', 'Next' and 'Last' buttons
+ * * `full_numbers` - 'First', 'Previous', 'Next' and 'Last' buttons, plus page numbers
+ * * `first_last_numbers` - 'First' and 'Last' buttons, plus page numbers
+ *
+ * Further methods can be added using {@link DataTable.ext.oPagination}.
+ * @type string
+ * @default simple_numbers
+ *
+ * @dtopt Options
+ * @name DataTable.defaults.pagingType
+ *
+ * @example
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "pagingType": "full_numbers"
+ * } );
+ * } )
+ */
+ "sPaginationType": "simple_numbers",
+
+
+ /**
+ * Enable horizontal scrolling. When a table is too wide to fit into a
+ * certain layout, or you have a large number of columns in the table, you
+ * can enable x-scrolling to show the table in a viewport, which can be
+ * scrolled. This property can be `true` which will allow the table to
+ * scroll horizontally when needed, or any CSS unit, or a number (in which
+ * case it will be treated as a pixel measurement). Setting as simply `true`
+ * is recommended.
+ * @type boolean|string
+ * @default blank string - i.e. disabled
+ *
+ * @dtopt Features
+ * @name DataTable.defaults.scrollX
+ *
+ * @example
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "scrollX": true,
+ * "scrollCollapse": true
+ * } );
+ * } );
+ */
+ "sScrollX": "",
+
+
+ /**
+ * This property can be used to force a DataTable to use more width than it
+ * might otherwise do when x-scrolling is enabled. For example if you have a
+ * table which requires to be well spaced, this parameter is useful for
+ * "over-sizing" the table, and thus forcing scrolling. This property can by
+ * any CSS unit, or a number (in which case it will be treated as a pixel
+ * measurement).
+ * @type string
+ * @default blank string - i.e. disabled
+ *
+ * @dtopt Options
+ * @name DataTable.defaults.scrollXInner
+ *
+ * @example
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "scrollX": "100%",
+ * "scrollXInner": "110%"
+ * } );
+ * } );
+ */
+ "sScrollXInner": "",
+
+
+ /**
+ * Enable vertical scrolling. Vertical scrolling will constrain the DataTable
+ * to the given height, and enable scrolling for any data which overflows the
+ * current viewport. This can be used as an alternative to paging to display
+ * a lot of data in a small area (although paging and scrolling can both be
+ * enabled at the same time). This property can be any CSS unit, or a number
+ * (in which case it will be treated as a pixel measurement).
+ * @type string
+ * @default blank string - i.e. disabled
+ *
+ * @dtopt Features
+ * @name DataTable.defaults.scrollY
+ *
+ * @example
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "scrollY": "200px",
+ * "paginate": false
+ * } );
+ * } );
+ */
+ "sScrollY": "",
+
+
+ /**
+ * __Deprecated__ The functionality provided by this parameter has now been
+ * superseded by that provided through `ajax`, which should be used instead.
+ *
+ * Set the HTTP method that is used to make the Ajax call for server-side
+ * processing or Ajax sourced data.
+ * @type string
+ * @default GET
+ *
+ * @dtopt Options
+ * @dtopt Server-side
+ * @name DataTable.defaults.serverMethod
+ *
+ * @deprecated 1.10. Please use `ajax` for this functionality now.
+ */
+ "sServerMethod": "GET",
+
+
+ /**
+ * DataTables makes use of renderers when displaying HTML elements for
+ * a table. These renderers can be added or modified by plug-ins to
+ * generate suitable mark-up for a site. For example the Bootstrap
+ * integration plug-in for DataTables uses a paging button renderer to
+ * display pagination buttons in the mark-up required by Bootstrap.
+ *
+ * For further information about the renderers available see
+ * DataTable.ext.renderer
+ * @type string|object
+ * @default null
+ *
+ * @name DataTable.defaults.renderer
+ *
+ */
+ "renderer": null,
+
+
+ /**
+ * Set the data property name that DataTables should use to get a row's id
+ * to set as the `id` property in the node.
+ * @type string
+ * @default DT_RowId
+ *
+ * @name DataTable.defaults.rowId
+ */
+ "rowId": "DT_RowId"
+ };
+
+ _fnHungarianMap( DataTable.defaults );
+
+
+
+ /*
+ * Developer note - See note in model.defaults.js about the use of Hungarian
+ * notation and camel case.
+ */
+
+ /**
+ * Column options that can be given to DataTables at initialisation time.
+ * @namespace
+ */
+ DataTable.defaults.column = {
+ /**
+ * Define which column(s) an order will occur on for this column. This
+ * allows a column's ordering to take multiple columns into account when
+ * doing a sort or use the data from a different column. For example first
+ * name / last name columns make sense to do a multi-column sort over the
+ * two columns.
+ * @type array|int
+ * @default null Takes the value of the column index automatically
+ *
+ * @name DataTable.defaults.column.orderData
+ * @dtopt Columns
+ *
+ * @example
+ * // Using `columnDefs`
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "columnDefs": [
+ * { "orderData": [ 0, 1 ], "targets": [ 0 ] },
+ * { "orderData": [ 1, 0 ], "targets": [ 1 ] },
+ * { "orderData": 2, "targets": [ 2 ] }
+ * ]
+ * } );
+ * } );
+ *
+ * @example
+ * // Using `columns`
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "columns": [
+ * { "orderData": [ 0, 1 ] },
+ * { "orderData": [ 1, 0 ] },
+ * { "orderData": 2 },
+ * null,
+ * null
+ * ]
+ * } );
+ * } );
+ */
+ "aDataSort": null,
+ "iDataSort": -1,
+
+
+ /**
+ * You can control the default ordering direction, and even alter the
+ * behaviour of the sort handler (i.e. only allow ascending ordering etc)
+ * using this parameter.
+ * @type array
+ * @default [ 'asc', 'desc' ]
+ *
+ * @name DataTable.defaults.column.orderSequence
+ * @dtopt Columns
+ *
+ * @example
+ * // Using `columnDefs`
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "columnDefs": [
+ * { "orderSequence": [ "asc" ], "targets": [ 1 ] },
+ * { "orderSequence": [ "desc", "asc", "asc" ], "targets": [ 2 ] },
+ * { "orderSequence": [ "desc" ], "targets": [ 3 ] }
+ * ]
+ * } );
+ * } );
+ *
+ * @example
+ * // Using `columns`
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "columns": [
+ * null,
+ * { "orderSequence": [ "asc" ] },
+ * { "orderSequence": [ "desc", "asc", "asc" ] },
+ * { "orderSequence": [ "desc" ] },
+ * null
+ * ]
+ * } );
+ * } );
+ */
+ "asSorting": [ 'asc', 'desc' ],
+
+
+ /**
+ * Enable or disable filtering on the data in this column.
+ * @type boolean
+ * @default true
+ *
+ * @name DataTable.defaults.column.searchable
+ * @dtopt Columns
+ *
+ * @example
+ * // Using `columnDefs`
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "columnDefs": [
+ * { "searchable": false, "targets": [ 0 ] }
+ * ] } );
+ * } );
+ *
+ * @example
+ * // Using `columns`
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "columns": [
+ * { "searchable": false },
+ * null,
+ * null,
+ * null,
+ * null
+ * ] } );
+ * } );
+ */
+ "bSearchable": true,
+
+
+ /**
+ * Enable or disable ordering on this column.
+ * @type boolean
+ * @default true
+ *
+ * @name DataTable.defaults.column.orderable
+ * @dtopt Columns
+ *
+ * @example
+ * // Using `columnDefs`
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "columnDefs": [
+ * { "orderable": false, "targets": [ 0 ] }
+ * ] } );
+ * } );
+ *
+ * @example
+ * // Using `columns`
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "columns": [
+ * { "orderable": false },
+ * null,
+ * null,
+ * null,
+ * null
+ * ] } );
+ * } );
+ */
+ "bSortable": true,
+
+
+ /**
+ * Enable or disable the display of this column.
+ * @type boolean
+ * @default true
+ *
+ * @name DataTable.defaults.column.visible
+ * @dtopt Columns
+ *
+ * @example
+ * // Using `columnDefs`
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "columnDefs": [
+ * { "visible": false, "targets": [ 0 ] }
+ * ] } );
+ * } );
+ *
+ * @example
+ * // Using `columns`
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "columns": [
+ * { "visible": false },
+ * null,
+ * null,
+ * null,
+ * null
+ * ] } );
+ * } );
+ */
+ "bVisible": true,
+
+
+ /**
+ * Developer definable function that is called whenever a cell is created (Ajax source,
+ * etc) or processed for input (DOM source). This can be used as a compliment to mRender
+ * allowing you to modify the DOM element (add background colour for example) when the
+ * element is available.
+ * @type function
+ * @param {element} td The TD node that has been created
+ * @param {*} cellData The Data for the cell
+ * @param {array|object} rowData The data for the whole row
+ * @param {int} row The row index for the aoData data store
+ * @param {int} col The column index for aoColumns
+ *
+ * @name DataTable.defaults.column.createdCell
+ * @dtopt Columns
+ *
+ * @example
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "columnDefs": [ {
+ * "targets": [3],
+ * "createdCell": function (td, cellData, rowData, row, col) {
+ * if ( cellData == "1.7" ) {
+ * $(td).css('color', 'blue')
+ * }
+ * }
+ * } ]
+ * });
+ * } );
+ */
+ "fnCreatedCell": null,
+
+
+ /**
+ * This parameter has been replaced by `data` in DataTables to ensure naming
+ * consistency. `dataProp` can still be used, as there is backwards
+ * compatibility in DataTables for this option, but it is strongly
+ * recommended that you use `data` in preference to `dataProp`.
+ * @name DataTable.defaults.column.dataProp
+ */
+
+
+ /**
+ * This property can be used to read data from any data source property,
+ * including deeply nested objects / properties. `data` can be given in a
+ * number of different ways which effect its behaviour:
+ *
+ * * `integer` - treated as an array index for the data source. This is the
+ * default that DataTables uses (incrementally increased for each column).
+ * * `string` - read an object property from the data source. There are
+ * three 'special' options that can be used in the string to alter how
+ * DataTables reads the data from the source object:
+ * * `.` - Dotted Javascript notation. Just as you use a `.` in
+ * Javascript to read from nested objects, so to can the options
+ * specified in `data`. For example: `browser.version` or
+ * `browser.name`. If your object parameter name contains a period, use
+ * `\\` to escape it - i.e. `first\\.name`.
+ * * `[]` - Array notation. DataTables can automatically combine data
+ * from and array source, joining the data with the characters provided
+ * between the two brackets. For example: `name[, ]` would provide a
+ * comma-space separated list from the source array. If no characters
+ * are provided between the brackets, the original array source is
+ * returned.
+ * * `()` - Function notation. Adding `()` to the end of a parameter will
+ * execute a function of the name given. For example: `browser()` for a
+ * simple function on the data source, `browser.version()` for a
+ * function in a nested property or even `browser().version` to get an
+ * object property if the function called returns an object. Note that
+ * function notation is recommended for use in `render` rather than
+ * `data` as it is much simpler to use as a renderer.
+ * * `null` - use the original data source for the row rather than plucking
+ * data directly from it. This action has effects on two other
+ * initialisation options:
+ * * `defaultContent` - When null is given as the `data` option and
+ * `defaultContent` is specified for the column, the value defined by
+ * `defaultContent` will be used for the cell.
+ * * `render` - When null is used for the `data` option and the `render`
+ * option is specified for the column, the whole data source for the
+ * row is used for the renderer.
+ * * `function` - the function given will be executed whenever DataTables
+ * needs to set or get the data for a cell in the column. The function
+ * takes three parameters:
+ * * Parameters:
+ * * `{array|object}` The data source for the row
+ * * `{string}` The type call data requested - this will be 'set' when
+ * setting data or 'filter', 'display', 'type', 'sort' or undefined
+ * when gathering data. Note that when `undefined` is given for the
+ * type DataTables expects to get the raw data for the object back<
+ * * `{*}` Data to set when the second parameter is 'set'.
+ * * Return:
+ * * The return value from the function is not required when 'set' is
+ * the type of call, but otherwise the return is what will be used
+ * for the data requested.
+ *
+ * Note that `data` is a getter and setter option. If you just require
+ * formatting of data for output, you will likely want to use `render` which
+ * is simply a getter and thus simpler to use.
+ *
+ * Note that prior to DataTables 1.9.2 `data` was called `mDataProp`. The
+ * name change reflects the flexibility of this property and is consistent
+ * with the naming of mRender. If 'mDataProp' is given, then it will still
+ * be used by DataTables, as it automatically maps the old name to the new
+ * if required.
+ *
+ * @type string|int|function|null
+ * @default null Use automatically calculated column index
+ *
+ * @name DataTable.defaults.column.data
+ * @dtopt Columns
+ *
+ * @example
+ * // Read table data from objects
+ * // JSON structure for each row:
+ * // {
+ * // "engine": {value},
+ * // "browser": {value},
+ * // "platform": {value},
+ * // "version": {value},
+ * // "grade": {value}
+ * // }
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "ajaxSource": "sources/objects.txt",
+ * "columns": [
+ * { "data": "engine" },
+ * { "data": "browser" },
+ * { "data": "platform" },
+ * { "data": "version" },
+ * { "data": "grade" }
+ * ]
+ * } );
+ * } );
+ *
+ * @example
+ * // Read information from deeply nested objects
+ * // JSON structure for each row:
+ * // {
+ * // "engine": {value},
+ * // "browser": {value},
+ * // "platform": {
+ * // "inner": {value}
+ * // },
+ * // "details": [
+ * // {value}, {value}
+ * // ]
+ * // }
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "ajaxSource": "sources/deep.txt",
+ * "columns": [
+ * { "data": "engine" },
+ * { "data": "browser" },
+ * { "data": "platform.inner" },
+ * { "data": "details.0" },
+ * { "data": "details.1" }
+ * ]
+ * } );
+ * } );
+ *
+ * @example
+ * // Using `data` as a function to provide different information for
+ * // sorting, filtering and display. In this case, currency (price)
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "columnDefs": [ {
+ * "targets": [ 0 ],
+ * "data": function ( source, type, val ) {
+ * if (type === 'set') {
+ * source.price = val;
+ * // Store the computed dislay and filter values for efficiency
+ * source.price_display = val=="" ? "" : "$"+numberFormat(val);
+ * source.price_filter = val=="" ? "" : "$"+numberFormat(val)+" "+val;
+ * return;
+ * }
+ * else if (type === 'display') {
+ * return source.price_display;
+ * }
+ * else if (type === 'filter') {
+ * return source.price_filter;
+ * }
+ * // 'sort', 'type' and undefined all just use the integer
+ * return source.price;
+ * }
+ * } ]
+ * } );
+ * } );
+ *
+ * @example
+ * // Using default content
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "columnDefs": [ {
+ * "targets": [ 0 ],
+ * "data": null,
+ * "defaultContent": "Click to edit"
+ * } ]
+ * } );
+ * } );
+ *
+ * @example
+ * // Using array notation - outputting a list from an array
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "columnDefs": [ {
+ * "targets": [ 0 ],
+ * "data": "name[, ]"
+ * } ]
+ * } );
+ * } );
+ *
+ */
+ "mData": null,
+
+
+ /**
+ * This property is the rendering partner to `data` and it is suggested that
+ * when you want to manipulate data for display (including filtering,
+ * sorting etc) without altering the underlying data for the table, use this
+ * property. `render` can be considered to be the the read only companion to
+ * `data` which is read / write (then as such more complex). Like `data`
+ * this option can be given in a number of different ways to effect its
+ * behaviour:
+ *
+ * * `integer` - treated as an array index for the data source. This is the
+ * default that DataTables uses (incrementally increased for each column).
+ * * `string` - read an object property from the data source. There are
+ * three 'special' options that can be used in the string to alter how
+ * DataTables reads the data from the source object:
+ * * `.` - Dotted Javascript notation. Just as you use a `.` in
+ * Javascript to read from nested objects, so to can the options
+ * specified in `data`. For example: `browser.version` or
+ * `browser.name`. If your object parameter name contains a period, use
+ * `\\` to escape it - i.e. `first\\.name`.
+ * * `[]` - Array notation. DataTables can automatically combine data
+ * from and array source, joining the data with the characters provided
+ * between the two brackets. For example: `name[, ]` would provide a
+ * comma-space separated list from the source array. If no characters
+ * are provided between the brackets, the original array source is
+ * returned.
+ * * `()` - Function notation. Adding `()` to the end of a parameter will
+ * execute a function of the name given. For example: `browser()` for a
+ * simple function on the data source, `browser.version()` for a
+ * function in a nested property or even `browser().version` to get an
+ * object property if the function called returns an object.
+ * * `object` - use different data for the different data types requested by
+ * DataTables ('filter', 'display', 'type' or 'sort'). The property names
+ * of the object is the data type the property refers to and the value can
+ * defined using an integer, string or function using the same rules as
+ * `render` normally does. Note that an `_` option _must_ be specified.
+ * This is the default value to use if you haven't specified a value for
+ * the data type requested by DataTables.
+ * * `function` - the function given will be executed whenever DataTables
+ * needs to set or get the data for a cell in the column. The function
+ * takes three parameters:
+ * * Parameters:
+ * * {array|object} The data source for the row (based on `data`)
+ * * {string} The type call data requested - this will be 'filter',
+ * 'display', 'type' or 'sort'.
+ * * {array|object} The full data source for the row (not based on
+ * `data`)
+ * * Return:
+ * * The return value from the function is what will be used for the
+ * data requested.
+ *
+ * @type string|int|function|object|null
+ * @default null Use the data source value.
+ *
+ * @name DataTable.defaults.column.render
+ * @dtopt Columns
+ *
+ * @example
+ * // Create a comma separated list from an array of objects
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "ajaxSource": "sources/deep.txt",
+ * "columns": [
+ * { "data": "engine" },
+ * { "data": "browser" },
+ * {
+ * "data": "platform",
+ * "render": "[, ].name"
+ * }
+ * ]
+ * } );
+ * } );
+ *
+ * @example
+ * // Execute a function to obtain data
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "columnDefs": [ {
+ * "targets": [ 0 ],
+ * "data": null, // Use the full data source object for the renderer's source
+ * "render": "browserName()"
+ * } ]
+ * } );
+ * } );
+ *
+ * @example
+ * // As an object, extracting different data for the different types
+ * // This would be used with a data source such as:
+ * // { "phone": 5552368, "phone_filter": "5552368 555-2368", "phone_display": "555-2368" }
+ * // Here the `phone` integer is used for sorting and type detection, while `phone_filter`
+ * // (which has both forms) is used for filtering for if a user inputs either format, while
+ * // the formatted phone number is the one that is shown in the table.
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "columnDefs": [ {
+ * "targets": [ 0 ],
+ * "data": null, // Use the full data source object for the renderer's source
+ * "render": {
+ * "_": "phone",
+ * "filter": "phone_filter",
+ * "display": "phone_display"
+ * }
+ * } ]
+ * } );
+ * } );
+ *
+ * @example
+ * // Use as a function to create a link from the data source
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "columnDefs": [ {
+ * "targets": [ 0 ],
+ * "data": "download_link",
+ * "render": function ( data, type, full ) {
+ * return 'Download ';
+ * }
+ * } ]
+ * } );
+ * } );
+ */
+ "mRender": null,
+
+
+ /**
+ * Change the cell type created for the column - either TD cells or TH cells. This
+ * can be useful as TH cells have semantic meaning in the table body, allowing them
+ * to act as a header for a row (you may wish to add scope='row' to the TH elements).
+ * @type string
+ * @default td
+ *
+ * @name DataTable.defaults.column.cellType
+ * @dtopt Columns
+ *
+ * @example
+ * // Make the first column use TH cells
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "columnDefs": [ {
+ * "targets": [ 0 ],
+ * "cellType": "th"
+ * } ]
+ * } );
+ * } );
+ */
+ "sCellType": "td",
+
+
+ /**
+ * Class to give to each cell in this column.
+ * @type string
+ * @default Empty string
+ *
+ * @name DataTable.defaults.column.class
+ * @dtopt Columns
+ *
+ * @example
+ * // Using `columnDefs`
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "columnDefs": [
+ * { "class": "my_class", "targets": [ 0 ] }
+ * ]
+ * } );
+ * } );
+ *
+ * @example
+ * // Using `columns`
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "columns": [
+ * { "class": "my_class" },
+ * null,
+ * null,
+ * null,
+ * null
+ * ]
+ * } );
+ * } );
+ */
+ "sClass": "",
+
+ /**
+ * When DataTables calculates the column widths to assign to each column,
+ * it finds the longest string in each column and then constructs a
+ * temporary table and reads the widths from that. The problem with this
+ * is that "mmm" is much wider then "iiii", but the latter is a longer
+ * string - thus the calculation can go wrong (doing it properly and putting
+ * it into an DOM object and measuring that is horribly(!) slow). Thus as
+ * a "work around" we provide this option. It will append its value to the
+ * text that is found to be the longest string for the column - i.e. padding.
+ * Generally you shouldn't need this!
+ * @type string
+ * @default Empty string
+ *
+ * @name DataTable.defaults.column.contentPadding
+ * @dtopt Columns
+ *
+ * @example
+ * // Using `columns`
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "columns": [
+ * null,
+ * null,
+ * null,
+ * {
+ * "contentPadding": "mmm"
+ * }
+ * ]
+ * } );
+ * } );
+ */
+ "sContentPadding": "",
+
+
+ /**
+ * Allows a default value to be given for a column's data, and will be used
+ * whenever a null data source is encountered (this can be because `data`
+ * is set to null, or because the data source itself is null).
+ * @type string
+ * @default null
+ *
+ * @name DataTable.defaults.column.defaultContent
+ * @dtopt Columns
+ *
+ * @example
+ * // Using `columnDefs`
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "columnDefs": [
+ * {
+ * "data": null,
+ * "defaultContent": "Edit",
+ * "targets": [ -1 ]
+ * }
+ * ]
+ * } );
+ * } );
+ *
+ * @example
+ * // Using `columns`
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "columns": [
+ * null,
+ * null,
+ * null,
+ * {
+ * "data": null,
+ * "defaultContent": "Edit"
+ * }
+ * ]
+ * } );
+ * } );
+ */
+ "sDefaultContent": null,
+
+
+ /**
+ * This parameter is only used in DataTables' server-side processing. It can
+ * be exceptionally useful to know what columns are being displayed on the
+ * client side, and to map these to database fields. When defined, the names
+ * also allow DataTables to reorder information from the server if it comes
+ * back in an unexpected order (i.e. if you switch your columns around on the
+ * client-side, your server-side code does not also need updating).
+ * @type string
+ * @default Empty string
+ *
+ * @name DataTable.defaults.column.name
+ * @dtopt Columns
+ *
+ * @example
+ * // Using `columnDefs`
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "columnDefs": [
+ * { "name": "engine", "targets": [ 0 ] },
+ * { "name": "browser", "targets": [ 1 ] },
+ * { "name": "platform", "targets": [ 2 ] },
+ * { "name": "version", "targets": [ 3 ] },
+ * { "name": "grade", "targets": [ 4 ] }
+ * ]
+ * } );
+ * } );
+ *
+ * @example
+ * // Using `columns`
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "columns": [
+ * { "name": "engine" },
+ * { "name": "browser" },
+ * { "name": "platform" },
+ * { "name": "version" },
+ * { "name": "grade" }
+ * ]
+ * } );
+ * } );
+ */
+ "sName": "",
+
+
+ /**
+ * Defines a data source type for the ordering which can be used to read
+ * real-time information from the table (updating the internally cached
+ * version) prior to ordering. This allows ordering to occur on user
+ * editable elements such as form inputs.
+ * @type string
+ * @default std
+ *
+ * @name DataTable.defaults.column.orderDataType
+ * @dtopt Columns
+ *
+ * @example
+ * // Using `columnDefs`
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "columnDefs": [
+ * { "orderDataType": "dom-text", "targets": [ 2, 3 ] },
+ * { "type": "numeric", "targets": [ 3 ] },
+ * { "orderDataType": "dom-select", "targets": [ 4 ] },
+ * { "orderDataType": "dom-checkbox", "targets": [ 5 ] }
+ * ]
+ * } );
+ * } );
+ *
+ * @example
+ * // Using `columns`
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "columns": [
+ * null,
+ * null,
+ * { "orderDataType": "dom-text" },
+ * { "orderDataType": "dom-text", "type": "numeric" },
+ * { "orderDataType": "dom-select" },
+ * { "orderDataType": "dom-checkbox" }
+ * ]
+ * } );
+ * } );
+ */
+ "sSortDataType": "std",
+
+
+ /**
+ * The title of this column.
+ * @type string
+ * @default null Derived from the 'TH' value for this column in the
+ * original HTML table.
+ *
+ * @name DataTable.defaults.column.title
+ * @dtopt Columns
+ *
+ * @example
+ * // Using `columnDefs`
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "columnDefs": [
+ * { "title": "My column title", "targets": [ 0 ] }
+ * ]
+ * } );
+ * } );
+ *
+ * @example
+ * // Using `columns`
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "columns": [
+ * { "title": "My column title" },
+ * null,
+ * null,
+ * null,
+ * null
+ * ]
+ * } );
+ * } );
+ */
+ "sTitle": null,
+
+
+ /**
+ * The type allows you to specify how the data for this column will be
+ * ordered. Four types (string, numeric, date and html (which will strip
+ * HTML tags before ordering)) are currently available. Note that only date
+ * formats understood by Javascript's Date() object will be accepted as type
+ * date. For example: "Mar 26, 2008 5:03 PM". May take the values: 'string',
+ * 'numeric', 'date' or 'html' (by default). Further types can be adding
+ * through plug-ins.
+ * @type string
+ * @default null Auto-detected from raw data
+ *
+ * @name DataTable.defaults.column.type
+ * @dtopt Columns
+ *
+ * @example
+ * // Using `columnDefs`
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "columnDefs": [
+ * { "type": "html", "targets": [ 0 ] }
+ * ]
+ * } );
+ * } );
+ *
+ * @example
+ * // Using `columns`
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "columns": [
+ * { "type": "html" },
+ * null,
+ * null,
+ * null,
+ * null
+ * ]
+ * } );
+ * } );
+ */
+ "sType": null,
+
+
+ /**
+ * Defining the width of the column, this parameter may take any CSS value
+ * (3em, 20px etc). DataTables applies 'smart' widths to columns which have not
+ * been given a specific width through this interface ensuring that the table
+ * remains readable.
+ * @type string
+ * @default null Automatic
+ *
+ * @name DataTable.defaults.column.width
+ * @dtopt Columns
+ *
+ * @example
+ * // Using `columnDefs`
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "columnDefs": [
+ * { "width": "20%", "targets": [ 0 ] }
+ * ]
+ * } );
+ * } );
+ *
+ * @example
+ * // Using `columns`
+ * $(document).ready( function() {
+ * $('#example').dataTable( {
+ * "columns": [
+ * { "width": "20%" },
+ * null,
+ * null,
+ * null,
+ * null
+ * ]
+ * } );
+ * } );
+ */
+ "sWidth": null
+ };
+
+ _fnHungarianMap( DataTable.defaults.column );
+
+
+
+ /**
+ * DataTables settings object - this holds all the information needed for a
+ * given table, including configuration, data and current application of the
+ * table options. DataTables does not have a single instance for each DataTable
+ * with the settings attached to that instance, but rather instances of the
+ * DataTable "class" are created on-the-fly as needed (typically by a
+ * $().dataTable() call) and the settings object is then applied to that
+ * instance.
+ *
+ * Note that this object is related to {@link DataTable.defaults} but this
+ * one is the internal data store for DataTables's cache of columns. It should
+ * NOT be manipulated outside of DataTables. Any configuration should be done
+ * through the initialisation options.
+ * @namespace
+ * @todo Really should attach the settings object to individual instances so we
+ * don't need to create new instances on each $().dataTable() call (if the
+ * table already exists). It would also save passing oSettings around and
+ * into every single function. However, this is a very significant
+ * architecture change for DataTables and will almost certainly break
+ * backwards compatibility with older installations. This is something that
+ * will be done in 2.0.
+ */
+ DataTable.models.oSettings = {
+ /**
+ * Primary features of DataTables and their enablement state.
+ * @namespace
+ */
+ "oFeatures": {
+
+ /**
+ * Flag to say if DataTables should automatically try to calculate the
+ * optimum table and columns widths (true) or not (false).
+ * Note that this parameter will be set by the initialisation routine. To
+ * set a default use {@link DataTable.defaults}.
+ * @type boolean
+ */
+ "bAutoWidth": null,
+
+ /**
+ * Delay the creation of TR and TD elements until they are actually
+ * needed by a driven page draw. This can give a significant speed
+ * increase for Ajax source and Javascript source data, but makes no
+ * difference at all fro DOM and server-side processing tables.
+ * Note that this parameter will be set by the initialisation routine. To
+ * set a default use {@link DataTable.defaults}.
+ * @type boolean
+ */
+ "bDeferRender": null,
+
+ /**
+ * Enable filtering on the table or not. Note that if this is disabled
+ * then there is no filtering at all on the table, including fnFilter.
+ * To just remove the filtering input use sDom and remove the 'f' option.
+ * Note that this parameter will be set by the initialisation routine. To
+ * set a default use {@link DataTable.defaults}.
+ * @type boolean
+ */
+ "bFilter": null,
+
+ /**
+ * Table information element (the 'Showing x of y records' div) enable
+ * flag.
+ * Note that this parameter will be set by the initialisation routine. To
+ * set a default use {@link DataTable.defaults}.
+ * @type boolean
+ */
+ "bInfo": null,
+
+ /**
+ * Present a user control allowing the end user to change the page size
+ * when pagination is enabled.
+ * Note that this parameter will be set by the initialisation routine. To
+ * set a default use {@link DataTable.defaults}.
+ * @type boolean
+ */
+ "bLengthChange": null,
+
+ /**
+ * Pagination enabled or not. Note that if this is disabled then length
+ * changing must also be disabled.
+ * Note that this parameter will be set by the initialisation routine. To
+ * set a default use {@link DataTable.defaults}.
+ * @type boolean
+ */
+ "bPaginate": null,
+
+ /**
+ * Processing indicator enable flag whenever DataTables is enacting a
+ * user request - typically an Ajax request for server-side processing.
+ * Note that this parameter will be set by the initialisation routine. To
+ * set a default use {@link DataTable.defaults}.
+ * @type boolean
+ */
+ "bProcessing": null,
+
+ /**
+ * Server-side processing enabled flag - when enabled DataTables will
+ * get all data from the server for every draw - there is no filtering,
+ * sorting or paging done on the client-side.
+ * Note that this parameter will be set by the initialisation routine. To
+ * set a default use {@link DataTable.defaults}.
+ * @type boolean
+ */
+ "bServerSide": null,
+
+ /**
+ * Sorting enablement flag.
+ * Note that this parameter will be set by the initialisation routine. To
+ * set a default use {@link DataTable.defaults}.
+ * @type boolean
+ */
+ "bSort": null,
+
+ /**
+ * Multi-column sorting
+ * Note that this parameter will be set by the initialisation routine. To
+ * set a default use {@link DataTable.defaults}.
+ * @type boolean
+ */
+ "bSortMulti": null,
+
+ /**
+ * Apply a class to the columns which are being sorted to provide a
+ * visual highlight or not. This can slow things down when enabled since
+ * there is a lot of DOM interaction.
+ * Note that this parameter will be set by the initialisation routine. To
+ * set a default use {@link DataTable.defaults}.
+ * @type boolean
+ */
+ "bSortClasses": null,
+
+ /**
+ * State saving enablement flag.
+ * Note that this parameter will be set by the initialisation routine. To
+ * set a default use {@link DataTable.defaults}.
+ * @type boolean
+ */
+ "bStateSave": null
+ },
+
+
+ /**
+ * Scrolling settings for a table.
+ * @namespace
+ */
+ "oScroll": {
+ /**
+ * When the table is shorter in height than sScrollY, collapse the
+ * table container down to the height of the table (when true).
+ * Note that this parameter will be set by the initialisation routine. To
+ * set a default use {@link DataTable.defaults}.
+ * @type boolean
+ */
+ "bCollapse": null,
+
+ /**
+ * Width of the scrollbar for the web-browser's platform. Calculated
+ * during table initialisation.
+ * @type int
+ * @default 0
+ */
+ "iBarWidth": 0,
+
+ /**
+ * Viewport width for horizontal scrolling. Horizontal scrolling is
+ * disabled if an empty string.
+ * Note that this parameter will be set by the initialisation routine. To
+ * set a default use {@link DataTable.defaults}.
+ * @type string
+ */
+ "sX": null,
+
+ /**
+ * Width to expand the table to when using x-scrolling. Typically you
+ * should not need to use this.
+ * Note that this parameter will be set by the initialisation routine. To
+ * set a default use {@link DataTable.defaults}.
+ * @type string
+ * @deprecated
+ */
+ "sXInner": null,
+
+ /**
+ * Viewport height for vertical scrolling. Vertical scrolling is disabled
+ * if an empty string.
+ * Note that this parameter will be set by the initialisation routine. To
+ * set a default use {@link DataTable.defaults}.
+ * @type string
+ */
+ "sY": null
+ },
+
+ /**
+ * Language information for the table.
+ * @namespace
+ * @extends DataTable.defaults.oLanguage
+ */
+ "oLanguage": {
+ /**
+ * Information callback function. See
+ * {@link DataTable.defaults.fnInfoCallback}
+ * @type function
+ * @default null
+ */
+ "fnInfoCallback": null
+ },
+
+ /**
+ * Browser support parameters
+ * @namespace
+ */
+ "oBrowser": {
+ /**
+ * Indicate if the browser incorrectly calculates width:100% inside a
+ * scrolling element (IE6/7)
+ * @type boolean
+ * @default false
+ */
+ "bScrollOversize": false,
+
+ /**
+ * Determine if the vertical scrollbar is on the right or left of the
+ * scrolling container - needed for rtl language layout, although not
+ * all browsers move the scrollbar (Safari).
+ * @type boolean
+ * @default false
+ */
+ "bScrollbarLeft": false,
+
+ /**
+ * Flag for if `getBoundingClientRect` is fully supported or not
+ * @type boolean
+ * @default false
+ */
+ "bBounding": false,
+
+ /**
+ * Browser scrollbar width
+ * @type integer
+ * @default 0
+ */
+ "barWidth": 0
+ },
+
+
+ "ajax": null,
+
+
+ /**
+ * Array referencing the nodes which are used for the features. The
+ * parameters of this object match what is allowed by sDom - i.e.
+ *
+ * 'l' - Length changing
+ * 'f' - Filtering input
+ * 't' - The table!
+ * 'i' - Information
+ * 'p' - Pagination
+ * 'r' - pRocessing
+ *
+ * @type array
+ * @default []
+ */
+ "aanFeatures": [],
+
+ /**
+ * Store data information - see {@link DataTable.models.oRow} for detailed
+ * information.
+ * @type array
+ * @default []
+ */
+ "aoData": [],
+
+ /**
+ * Array of indexes which are in the current display (after filtering etc)
+ * @type array
+ * @default []
+ */
+ "aiDisplay": [],
+
+ /**
+ * Array of indexes for display - no filtering
+ * @type array
+ * @default []
+ */
+ "aiDisplayMaster": [],
+
+ /**
+ * Map of row ids to data indexes
+ * @type object
+ * @default {}
+ */
+ "aIds": {},
+
+ /**
+ * Store information about each column that is in use
+ * @type array
+ * @default []
+ */
+ "aoColumns": [],
+
+ /**
+ * Store information about the table's header
+ * @type array
+ * @default []
+ */
+ "aoHeader": [],
+
+ /**
+ * Store information about the table's footer
+ * @type array
+ * @default []
+ */
+ "aoFooter": [],
+
+ /**
+ * Store the applied global search information in case we want to force a
+ * research or compare the old search to a new one.
+ * Note that this parameter will be set by the initialisation routine. To
+ * set a default use {@link DataTable.defaults}.
+ * @namespace
+ * @extends DataTable.models.oSearch
+ */
+ "oPreviousSearch": {},
+
+ /**
+ * Store the applied search for each column - see
+ * {@link DataTable.models.oSearch} for the format that is used for the
+ * filtering information for each column.
+ * @type array
+ * @default []
+ */
+ "aoPreSearchCols": [],
+
+ /**
+ * Sorting that is applied to the table. Note that the inner arrays are
+ * used in the following manner:
+ *
+ * Index 0 - column number
+ * Index 1 - current sorting direction
+ *
+ * Note that this parameter will be set by the initialisation routine. To
+ * set a default use {@link DataTable.defaults}.
+ * @type array
+ * @todo These inner arrays should really be objects
+ */
+ "aaSorting": null,
+
+ /**
+ * Sorting that is always applied to the table (i.e. prefixed in front of
+ * aaSorting).
+ * Note that this parameter will be set by the initialisation routine. To
+ * set a default use {@link DataTable.defaults}.
+ * @type array
+ * @default []
+ */
+ "aaSortingFixed": [],
+
+ /**
+ * Classes to use for the striping of a table.
+ * Note that this parameter will be set by the initialisation routine. To
+ * set a default use {@link DataTable.defaults}.
+ * @type array
+ * @default []
+ */
+ "asStripeClasses": null,
+
+ /**
+ * If restoring a table - we should restore its striping classes as well
+ * @type array
+ * @default []
+ */
+ "asDestroyStripes": [],
+
+ /**
+ * If restoring a table - we should restore its width
+ * @type int
+ * @default 0
+ */
+ "sDestroyWidth": 0,
+
+ /**
+ * Callback functions array for every time a row is inserted (i.e. on a draw).
+ * @type array
+ * @default []
+ */
+ "aoRowCallback": [],
+
+ /**
+ * Callback functions for the header on each draw.
+ * @type array
+ * @default []
+ */
+ "aoHeaderCallback": [],
+
+ /**
+ * Callback function for the footer on each draw.
+ * @type array
+ * @default []
+ */
+ "aoFooterCallback": [],
+
+ /**
+ * Array of callback functions for draw callback functions
+ * @type array
+ * @default []
+ */
+ "aoDrawCallback": [],
+
+ /**
+ * Array of callback functions for row created function
+ * @type array
+ * @default []
+ */
+ "aoRowCreatedCallback": [],
+
+ /**
+ * Callback functions for just before the table is redrawn. A return of
+ * false will be used to cancel the draw.
+ * @type array
+ * @default []
+ */
+ "aoPreDrawCallback": [],
+
+ /**
+ * Callback functions for when the table has been initialised.
+ * @type array
+ * @default []
+ */
+ "aoInitComplete": [],
+
+
+ /**
+ * Callbacks for modifying the settings to be stored for state saving, prior to
+ * saving state.
+ * @type array
+ * @default []
+ */
+ "aoStateSaveParams": [],
+
+ /**
+ * Callbacks for modifying the settings that have been stored for state saving
+ * prior to using the stored values to restore the state.
+ * @type array
+ * @default []
+ */
+ "aoStateLoadParams": [],
+
+ /**
+ * Callbacks for operating on the settings object once the saved state has been
+ * loaded
+ * @type array
+ * @default []
+ */
+ "aoStateLoaded": [],
+
+ /**
+ * Cache the table ID for quick access
+ * @type string
+ * @default Empty string
+ */
+ "sTableId": "",
+
+ /**
+ * The TABLE node for the main table
+ * @type node
+ * @default null
+ */
+ "nTable": null,
+
+ /**
+ * Permanent ref to the thead element
+ * @type node
+ * @default null
+ */
+ "nTHead": null,
+
+ /**
+ * Permanent ref to the tfoot element - if it exists
+ * @type node
+ * @default null
+ */
+ "nTFoot": null,
+
+ /**
+ * Permanent ref to the tbody element
+ * @type node
+ * @default null
+ */
+ "nTBody": null,
+
+ /**
+ * Cache the wrapper node (contains all DataTables controlled elements)
+ * @type node
+ * @default null
+ */
+ "nTableWrapper": null,
+
+ /**
+ * Indicate if when using server-side processing the loading of data
+ * should be deferred until the second draw.
+ * Note that this parameter will be set by the initialisation routine. To
+ * set a default use {@link DataTable.defaults}.
+ * @type boolean
+ * @default false
+ */
+ "bDeferLoading": false,
+
+ /**
+ * Indicate if all required information has been read in
+ * @type boolean
+ * @default false
+ */
+ "bInitialised": false,
+
+ /**
+ * Information about open rows. Each object in the array has the parameters
+ * 'nTr' and 'nParent'
+ * @type array
+ * @default []
+ */
+ "aoOpenRows": [],
+
+ /**
+ * Dictate the positioning of DataTables' control elements - see
+ * {@link DataTable.model.oInit.sDom}.
+ * Note that this parameter will be set by the initialisation routine. To
+ * set a default use {@link DataTable.defaults}.
+ * @type string
+ * @default null
+ */
+ "sDom": null,
+
+ /**
+ * Search delay (in mS)
+ * @type integer
+ * @default null
+ */
+ "searchDelay": null,
+
+ /**
+ * Which type of pagination should be used.
+ * Note that this parameter will be set by the initialisation routine. To
+ * set a default use {@link DataTable.defaults}.
+ * @type string
+ * @default two_button
+ */
+ "sPaginationType": "two_button",
+
+ /**
+ * The state duration (for `stateSave`) in seconds.
+ * Note that this parameter will be set by the initialisation routine. To
+ * set a default use {@link DataTable.defaults}.
+ * @type int
+ * @default 0
+ */
+ "iStateDuration": 0,
+
+ /**
+ * Array of callback functions for state saving. Each array element is an
+ * object with the following parameters:
+ *
+ * function:fn - function to call. Takes two parameters, oSettings
+ * and the JSON string to save that has been thus far created. Returns
+ * a JSON string to be inserted into a json object
+ * (i.e. '"param": [ 0, 1, 2]')
+ * string:sName - name of callback
+ *
+ * @type array
+ * @default []
+ */
+ "aoStateSave": [],
+
+ /**
+ * Array of callback functions for state loading. Each array element is an
+ * object with the following parameters:
+ *
+ * function:fn - function to call. Takes two parameters, oSettings
+ * and the object stored. May return false to cancel state loading
+ * string:sName - name of callback
+ *
+ * @type array
+ * @default []
+ */
+ "aoStateLoad": [],
+
+ /**
+ * State that was saved. Useful for back reference
+ * @type object
+ * @default null
+ */
+ "oSavedState": null,
+
+ /**
+ * State that was loaded. Useful for back reference
+ * @type object
+ * @default null
+ */
+ "oLoadedState": null,
+
+ /**
+ * Source url for AJAX data for the table.
+ * Note that this parameter will be set by the initialisation routine. To
+ * set a default use {@link DataTable.defaults}.
+ * @type string
+ * @default null
+ */
+ "sAjaxSource": null,
+
+ /**
+ * Property from a given object from which to read the table data from. This
+ * can be an empty string (when not server-side processing), in which case
+ * it is assumed an an array is given directly.
+ * Note that this parameter will be set by the initialisation routine. To
+ * set a default use {@link DataTable.defaults}.
+ * @type string
+ */
+ "sAjaxDataProp": null,
+
+ /**
+ * Note if draw should be blocked while getting data
+ * @type boolean
+ * @default true
+ */
+ "bAjaxDataGet": true,
+
+ /**
+ * The last jQuery XHR object that was used for server-side data gathering.
+ * This can be used for working with the XHR information in one of the
+ * callbacks
+ * @type object
+ * @default null
+ */
+ "jqXHR": null,
+
+ /**
+ * JSON returned from the server in the last Ajax request
+ * @type object
+ * @default undefined
+ */
+ "json": undefined,
+
+ /**
+ * Data submitted as part of the last Ajax request
+ * @type object
+ * @default undefined
+ */
+ "oAjaxData": undefined,
+
+ /**
+ * Function to get the server-side data.
+ * Note that this parameter will be set by the initialisation routine. To
+ * set a default use {@link DataTable.defaults}.
+ * @type function
+ */
+ "fnServerData": null,
+
+ /**
+ * Functions which are called prior to sending an Ajax request so extra
+ * parameters can easily be sent to the server
+ * @type array
+ * @default []
+ */
+ "aoServerParams": [],
+
+ /**
+ * Send the XHR HTTP method - GET or POST (could be PUT or DELETE if
+ * required).
+ * Note that this parameter will be set by the initialisation routine. To
+ * set a default use {@link DataTable.defaults}.
+ * @type string
+ */
+ "sServerMethod": null,
+
+ /**
+ * Format numbers for display.
+ * Note that this parameter will be set by the initialisation routine. To
+ * set a default use {@link DataTable.defaults}.
+ * @type function
+ */
+ "fnFormatNumber": null,
+
+ /**
+ * List of options that can be used for the user selectable length menu.
+ * Note that this parameter will be set by the initialisation routine. To
+ * set a default use {@link DataTable.defaults}.
+ * @type array
+ * @default []
+ */
+ "aLengthMenu": null,
+
+ /**
+ * Counter for the draws that the table does. Also used as a tracker for
+ * server-side processing
+ * @type int
+ * @default 0
+ */
+ "iDraw": 0,
+
+ /**
+ * Indicate if a redraw is being done - useful for Ajax
+ * @type boolean
+ * @default false
+ */
+ "bDrawing": false,
+
+ /**
+ * Draw index (iDraw) of the last error when parsing the returned data
+ * @type int
+ * @default -1
+ */
+ "iDrawError": -1,
+
+ /**
+ * Paging display length
+ * @type int
+ * @default 10
+ */
+ "_iDisplayLength": 10,
+
+ /**
+ * Paging start point - aiDisplay index
+ * @type int
+ * @default 0
+ */
+ "_iDisplayStart": 0,
+
+ /**
+ * Server-side processing - number of records in the result set
+ * (i.e. before filtering), Use fnRecordsTotal rather than
+ * this property to get the value of the number of records, regardless of
+ * the server-side processing setting.
+ * @type int
+ * @default 0
+ * @private
+ */
+ "_iRecordsTotal": 0,
+
+ /**
+ * Server-side processing - number of records in the current display set
+ * (i.e. after filtering). Use fnRecordsDisplay rather than
+ * this property to get the value of the number of records, regardless of
+ * the server-side processing setting.
+ * @type boolean
+ * @default 0
+ * @private
+ */
+ "_iRecordsDisplay": 0,
+
+ /**
+ * The classes to use for the table
+ * @type object
+ * @default {}
+ */
+ "oClasses": {},
+
+ /**
+ * Flag attached to the settings object so you can check in the draw
+ * callback if filtering has been done in the draw. Deprecated in favour of
+ * events.
+ * @type boolean
+ * @default false
+ * @deprecated
+ */
+ "bFiltered": false,
+
+ /**
+ * Flag attached to the settings object so you can check in the draw
+ * callback if sorting has been done in the draw. Deprecated in favour of
+ * events.
+ * @type boolean
+ * @default false
+ * @deprecated
+ */
+ "bSorted": false,
+
+ /**
+ * Indicate that if multiple rows are in the header and there is more than
+ * one unique cell per column, if the top one (true) or bottom one (false)
+ * should be used for sorting / title by DataTables.
+ * Note that this parameter will be set by the initialisation routine. To
+ * set a default use {@link DataTable.defaults}.
+ * @type boolean
+ */
+ "bSortCellsTop": null,
+
+ /**
+ * Initialisation object that is used for the table
+ * @type object
+ * @default null
+ */
+ "oInit": null,
+
+ /**
+ * Destroy callback functions - for plug-ins to attach themselves to the
+ * destroy so they can clean up markup and events.
+ * @type array
+ * @default []
+ */
+ "aoDestroyCallback": [],
+
+
+ /**
+ * Get the number of records in the current record set, before filtering
+ * @type function
+ */
+ "fnRecordsTotal": function ()
+ {
+ return _fnDataSource( this ) == 'ssp' ?
+ this._iRecordsTotal * 1 :
+ this.aiDisplayMaster.length;
+ },
+
+ /**
+ * Get the number of records in the current record set, after filtering
+ * @type function
+ */
+ "fnRecordsDisplay": function ()
+ {
+ return _fnDataSource( this ) == 'ssp' ?
+ this._iRecordsDisplay * 1 :
+ this.aiDisplay.length;
+ },
+
+ /**
+ * Get the display end point - aiDisplay index
+ * @type function
+ */
+ "fnDisplayEnd": function ()
+ {
+ var
+ len = this._iDisplayLength,
+ start = this._iDisplayStart,
+ calc = start + len,
+ records = this.aiDisplay.length,
+ features = this.oFeatures,
+ paginate = features.bPaginate;
+
+ if ( features.bServerSide ) {
+ return paginate === false || len === -1 ?
+ start + records :
+ Math.min( start+len, this._iRecordsDisplay );
+ }
+ else {
+ return ! paginate || calc>records || len===-1 ?
+ records :
+ calc;
+ }
+ },
+
+ /**
+ * The DataTables object for this table
+ * @type object
+ * @default null
+ */
+ "oInstance": null,
+
+ /**
+ * Unique identifier for each instance of the DataTables object. If there
+ * is an ID on the table node, then it takes that value, otherwise an
+ * incrementing internal counter is used.
+ * @type string
+ * @default null
+ */
+ "sInstance": null,
+
+ /**
+ * tabindex attribute value that is added to DataTables control elements, allowing
+ * keyboard navigation of the table and its controls.
+ */
+ "iTabIndex": 0,
+
+ /**
+ * DIV container for the footer scrolling table if scrolling
+ */
+ "nScrollHead": null,
+
+ /**
+ * DIV container for the footer scrolling table if scrolling
+ */
+ "nScrollFoot": null,
+
+ /**
+ * Last applied sort
+ * @type array
+ * @default []
+ */
+ "aLastSort": [],
+
+ /**
+ * Stored plug-in instances
+ * @type object
+ * @default {}
+ */
+ "oPlugins": {},
+
+ /**
+ * Function used to get a row's id from the row's data
+ * @type function
+ * @default null
+ */
+ "rowIdFn": null,
+
+ /**
+ * Data location where to store a row's id
+ * @type string
+ * @default null
+ */
+ "rowId": null
+ };
+
+ /**
+ * Extension object for DataTables that is used to provide all extension
+ * options.
+ *
+ * Note that the `DataTable.ext` object is available through
+ * `jQuery.fn.dataTable.ext` where it may be accessed and manipulated. It is
+ * also aliased to `jQuery.fn.dataTableExt` for historic reasons.
+ * @namespace
+ * @extends DataTable.models.ext
+ */
+
+
+ /**
+ * DataTables extensions
+ *
+ * This namespace acts as a collection area for plug-ins that can be used to
+ * extend DataTables capabilities. Indeed many of the build in methods
+ * use this method to provide their own capabilities (sorting methods for
+ * example).
+ *
+ * Note that this namespace is aliased to `jQuery.fn.dataTableExt` for legacy
+ * reasons
+ *
+ * @namespace
+ */
+ DataTable.ext = _ext = {
+ /**
+ * Buttons. For use with the Buttons extension for DataTables. This is
+ * defined here so other extensions can define buttons regardless of load
+ * order. It is _not_ used by DataTables core.
+ *
+ * @type object
+ * @default {}
+ */
+ buttons: {},
+
+
+ /**
+ * Element class names
+ *
+ * @type object
+ * @default {}
+ */
+ classes: {},
+
+
+ /**
+ * DataTables build type (expanded by the download builder)
+ *
+ * @type string
+ */
+ builder: "-source-",
+
+
+ /**
+ * Error reporting.
+ *
+ * How should DataTables report an error. Can take the value 'alert',
+ * 'throw', 'none' or a function.
+ *
+ * @type string|function
+ * @default alert
+ */
+ errMode: "alert",
+
+
+ /**
+ * Feature plug-ins.
+ *
+ * This is an array of objects which describe the feature plug-ins that are
+ * available to DataTables. These feature plug-ins are then available for
+ * use through the `dom` initialisation option.
+ *
+ * Each feature plug-in is described by an object which must have the
+ * following properties:
+ *
+ * * `fnInit` - function that is used to initialise the plug-in,
+ * * `cFeature` - a character so the feature can be enabled by the `dom`
+ * instillation option. This is case sensitive.
+ *
+ * The `fnInit` function has the following input parameters:
+ *
+ * 1. `{object}` DataTables settings object: see
+ * {@link DataTable.models.oSettings}
+ *
+ * And the following return is expected:
+ *
+ * * {node|null} The element which contains your feature. Note that the
+ * return may also be void if your plug-in does not require to inject any
+ * DOM elements into DataTables control (`dom`) - for example this might
+ * be useful when developing a plug-in which allows table control via
+ * keyboard entry
+ *
+ * @type array
+ *
+ * @example
+ * $.fn.dataTable.ext.features.push( {
+ * "fnInit": function( oSettings ) {
+ * return new TableTools( { "oDTSettings": oSettings } );
+ * },
+ * "cFeature": "T"
+ * } );
+ */
+ feature: [],
+
+
+ /**
+ * Row searching.
+ *
+ * This method of searching is complimentary to the default type based
+ * searching, and a lot more comprehensive as it allows you complete control
+ * over the searching logic. Each element in this array is a function
+ * (parameters described below) that is called for every row in the table,
+ * and your logic decides if it should be included in the searching data set
+ * or not.
+ *
+ * Searching functions have the following input parameters:
+ *
+ * 1. `{object}` DataTables settings object: see
+ * {@link DataTable.models.oSettings}
+ * 2. `{array|object}` Data for the row to be processed (same as the
+ * original format that was passed in as the data source, or an array
+ * from a DOM data source
+ * 3. `{int}` Row index ({@link DataTable.models.oSettings.aoData}), which
+ * can be useful to retrieve the `TR` element if you need DOM interaction.
+ *
+ * And the following return is expected:
+ *
+ * * {boolean} Include the row in the searched result set (true) or not
+ * (false)
+ *
+ * Note that as with the main search ability in DataTables, technically this
+ * is "filtering", since it is subtractive. However, for consistency in
+ * naming we call it searching here.
+ *
+ * @type array
+ * @default []
+ *
+ * @example
+ * // The following example shows custom search being applied to the
+ * // fourth column (i.e. the data[3] index) based on two input values
+ * // from the end-user, matching the data in a certain range.
+ * $.fn.dataTable.ext.search.push(
+ * function( settings, data, dataIndex ) {
+ * var min = document.getElementById('min').value * 1;
+ * var max = document.getElementById('max').value * 1;
+ * var version = data[3] == "-" ? 0 : data[3]*1;
+ *
+ * if ( min == "" && max == "" ) {
+ * return true;
+ * }
+ * else if ( min == "" && version < max ) {
+ * return true;
+ * }
+ * else if ( min < version && "" == max ) {
+ * return true;
+ * }
+ * else if ( min < version && version < max ) {
+ * return true;
+ * }
+ * return false;
+ * }
+ * );
+ */
+ search: [],
+
+
+ /**
+ * Selector extensions
+ *
+ * The `selector` option can be used to extend the options available for the
+ * selector modifier options (`selector-modifier` object data type) that
+ * each of the three built in selector types offer (row, column and cell +
+ * their plural counterparts). For example the Select extension uses this
+ * mechanism to provide an option to select only rows, columns and cells
+ * that have been marked as selected by the end user (`{selected: true}`),
+ * which can be used in conjunction with the existing built in selector
+ * options.
+ *
+ * Each property is an array to which functions can be pushed. The functions
+ * take three attributes:
+ *
+ * * Settings object for the host table
+ * * Options object (`selector-modifier` object type)
+ * * Array of selected item indexes
+ *
+ * The return is an array of the resulting item indexes after the custom
+ * selector has been applied.
+ *
+ * @type object
+ */
+ selector: {
+ cell: [],
+ column: [],
+ row: []
+ },
+
+
+ /**
+ * Internal functions, exposed for used in plug-ins.
+ *
+ * Please note that you should not need to use the internal methods for
+ * anything other than a plug-in (and even then, try to avoid if possible).
+ * The internal function may change between releases.
+ *
+ * @type object
+ * @default {}
+ */
+ internal: {},
+
+
+ /**
+ * Legacy configuration options. Enable and disable legacy options that
+ * are available in DataTables.
+ *
+ * @type object
+ */
+ legacy: {
+ /**
+ * Enable / disable DataTables 1.9 compatible server-side processing
+ * requests
+ *
+ * @type boolean
+ * @default null
+ */
+ ajax: null
+ },
+
+
+ /**
+ * Pagination plug-in methods.
+ *
+ * Each entry in this object is a function and defines which buttons should
+ * be shown by the pagination rendering method that is used for the table:
+ * {@link DataTable.ext.renderer.pageButton}. The renderer addresses how the
+ * buttons are displayed in the document, while the functions here tell it
+ * what buttons to display. This is done by returning an array of button
+ * descriptions (what each button will do).
+ *
+ * Pagination types (the four built in options and any additional plug-in
+ * options defined here) can be used through the `paginationType`
+ * initialisation parameter.
+ *
+ * The functions defined take two parameters:
+ *
+ * 1. `{int} page` The current page index
+ * 2. `{int} pages` The number of pages in the table
+ *
+ * Each function is expected to return an array where each element of the
+ * array can be one of:
+ *
+ * * `first` - Jump to first page when activated
+ * * `last` - Jump to last page when activated
+ * * `previous` - Show previous page when activated
+ * * `next` - Show next page when activated
+ * * `{int}` - Show page of the index given
+ * * `{array}` - A nested array containing the above elements to add a
+ * containing 'DIV' element (might be useful for styling).
+ *
+ * Note that DataTables v1.9- used this object slightly differently whereby
+ * an object with two functions would be defined for each plug-in. That
+ * ability is still supported by DataTables 1.10+ to provide backwards
+ * compatibility, but this option of use is now decremented and no longer
+ * documented in DataTables 1.10+.
+ *
+ * @type object
+ * @default {}
+ *
+ * @example
+ * // Show previous, next and current page buttons only
+ * $.fn.dataTableExt.oPagination.current = function ( page, pages ) {
+ * return [ 'previous', page, 'next' ];
+ * };
+ */
+ pager: {},
+
+
+ renderer: {
+ pageButton: {},
+ header: {}
+ },
+
+
+ /**
+ * Ordering plug-ins - custom data source
+ *
+ * The extension options for ordering of data available here is complimentary
+ * to the default type based ordering that DataTables typically uses. It
+ * allows much greater control over the the data that is being used to
+ * order a column, but is necessarily therefore more complex.
+ *
+ * This type of ordering is useful if you want to do ordering based on data
+ * live from the DOM (for example the contents of an 'input' element) rather
+ * than just the static string that DataTables knows of.
+ *
+ * The way these plug-ins work is that you create an array of the values you
+ * wish to be ordering for the column in question and then return that
+ * array. The data in the array much be in the index order of the rows in
+ * the table (not the currently ordering order!). Which order data gathering
+ * function is run here depends on the `dt-init columns.orderDataType`
+ * parameter that is used for the column (if any).
+ *
+ * The functions defined take two parameters:
+ *
+ * 1. `{object}` DataTables settings object: see
+ * {@link DataTable.models.oSettings}
+ * 2. `{int}` Target column index
+ *
+ * Each function is expected to return an array:
+ *
+ * * `{array}` Data for the column to be ordering upon
+ *
+ * @type array
+ *
+ * @example
+ * // Ordering using `input` node values
+ * $.fn.dataTable.ext.order['dom-text'] = function ( settings, col )
+ * {
+ * return this.api().column( col, {order:'index'} ).nodes().map( function ( td, i ) {
+ * return $('input', td).val();
+ * } );
+ * }
+ */
+ order: {},
+
+
+ /**
+ * Type based plug-ins.
+ *
+ * Each column in DataTables has a type assigned to it, either by automatic
+ * detection or by direct assignment using the `type` option for the column.
+ * The type of a column will effect how it is ordering and search (plug-ins
+ * can also make use of the column type if required).
+ *
+ * @namespace
+ */
+ type: {
+ /**
+ * Type detection functions.
+ *
+ * The functions defined in this object are used to automatically detect
+ * a column's type, making initialisation of DataTables super easy, even
+ * when complex data is in the table.
+ *
+ * The functions defined take two parameters:
+ *
+ * 1. `{*}` Data from the column cell to be analysed
+ * 2. `{settings}` DataTables settings object. This can be used to
+ * perform context specific type detection - for example detection
+ * based on language settings such as using a comma for a decimal
+ * place. Generally speaking the options from the settings will not
+ * be required
+ *
+ * Each function is expected to return:
+ *
+ * * `{string|null}` Data type detected, or null if unknown (and thus
+ * pass it on to the other type detection functions.
+ *
+ * @type array
+ *
+ * @example
+ * // Currency type detection plug-in:
+ * $.fn.dataTable.ext.type.detect.push(
+ * function ( data, settings ) {
+ * // Check the numeric part
+ * if ( ! data.substring(1).match(/[0-9]/) ) {
+ * return null;
+ * }
+ *
+ * // Check prefixed by currency
+ * if ( data.charAt(0) == '$' || data.charAt(0) == '£' ) {
+ * return 'currency';
+ * }
+ * return null;
+ * }
+ * );
+ */
+ detect: [],
+
+
+ /**
+ * Type based search formatting.
+ *
+ * The type based searching functions can be used to pre-format the
+ * data to be search on. For example, it can be used to strip HTML
+ * tags or to de-format telephone numbers for numeric only searching.
+ *
+ * Note that is a search is not defined for a column of a given type,
+ * no search formatting will be performed.
+ *
+ * Pre-processing of searching data plug-ins - When you assign the sType
+ * for a column (or have it automatically detected for you by DataTables
+ * or a type detection plug-in), you will typically be using this for
+ * custom sorting, but it can also be used to provide custom searching
+ * by allowing you to pre-processing the data and returning the data in
+ * the format that should be searched upon. This is done by adding
+ * functions this object with a parameter name which matches the sType
+ * for that target column. This is the corollary of afnSortData
+ * for searching data.
+ *
+ * The functions defined take a single parameter:
+ *
+ * 1. `{*}` Data from the column cell to be prepared for searching
+ *
+ * Each function is expected to return:
+ *
+ * * `{string|null}` Formatted string that will be used for the searching.
+ *
+ * @type object
+ * @default {}
+ *
+ * @example
+ * $.fn.dataTable.ext.type.search['title-numeric'] = function ( d ) {
+ * return d.replace(/\n/g," ").replace( /<.*?>/g, "" );
+ * }
+ */
+ search: {},
+
+
+ /**
+ * Type based ordering.
+ *
+ * The column type tells DataTables what ordering to apply to the table
+ * when a column is sorted upon. The order for each type that is defined,
+ * is defined by the functions available in this object.
+ *
+ * Each ordering option can be described by three properties added to
+ * this object:
+ *
+ * * `{type}-pre` - Pre-formatting function
+ * * `{type}-asc` - Ascending order function
+ * * `{type}-desc` - Descending order function
+ *
+ * All three can be used together, only `{type}-pre` or only
+ * `{type}-asc` and `{type}-desc` together. It is generally recommended
+ * that only `{type}-pre` is used, as this provides the optimal
+ * implementation in terms of speed, although the others are provided
+ * for compatibility with existing Javascript sort functions.
+ *
+ * `{type}-pre`: Functions defined take a single parameter:
+ *
+ * 1. `{*}` Data from the column cell to be prepared for ordering
+ *
+ * And return:
+ *
+ * * `{*}` Data to be sorted upon
+ *
+ * `{type}-asc` and `{type}-desc`: Functions are typical Javascript sort
+ * functions, taking two parameters:
+ *
+ * 1. `{*}` Data to compare to the second parameter
+ * 2. `{*}` Data to compare to the first parameter
+ *
+ * And returning:
+ *
+ * * `{*}` Ordering match: <0 if first parameter should be sorted lower
+ * than the second parameter, ===0 if the two parameters are equal and
+ * >0 if the first parameter should be sorted height than the second
+ * parameter.
+ *
+ * @type object
+ * @default {}
+ *
+ * @example
+ * // Numeric ordering of formatted numbers with a pre-formatter
+ * $.extend( $.fn.dataTable.ext.type.order, {
+ * "string-pre": function(x) {
+ * a = (a === "-" || a === "") ? 0 : a.replace( /[^\d\-\.]/g, "" );
+ * return parseFloat( a );
+ * }
+ * } );
+ *
+ * @example
+ * // Case-sensitive string ordering, with no pre-formatting method
+ * $.extend( $.fn.dataTable.ext.order, {
+ * "string-case-asc": function(x,y) {
+ * return ((x < y) ? -1 : ((x > y) ? 1 : 0));
+ * },
+ * "string-case-desc": function(x,y) {
+ * return ((x < y) ? 1 : ((x > y) ? -1 : 0));
+ * }
+ * } );
+ */
+ order: {}
+ },
+
+ /**
+ * Unique DataTables instance counter
+ *
+ * @type int
+ * @private
+ */
+ _unique: 0,
+
+
+ //
+ // Depreciated
+ // The following properties are retained for backwards compatiblity only.
+ // The should not be used in new projects and will be removed in a future
+ // version
+ //
+
+ /**
+ * Version check function.
+ * @type function
+ * @depreciated Since 1.10
+ */
+ fnVersionCheck: DataTable.fnVersionCheck,
+
+
+ /**
+ * Index for what 'this' index API functions should use
+ * @type int
+ * @deprecated Since v1.10
+ */
+ iApiIndex: 0,
+
+
+ /**
+ * jQuery UI class container
+ * @type object
+ * @deprecated Since v1.10
+ */
+ oJUIClasses: {},
+
+
+ /**
+ * Software version
+ * @type string
+ * @deprecated Since v1.10
+ */
+ sVersion: DataTable.version
+ };
+
+
+ //
+ // Backwards compatibility. Alias to pre 1.10 Hungarian notation counter parts
+ //
+ $.extend( _ext, {
+ afnFiltering: _ext.search,
+ aTypes: _ext.type.detect,
+ ofnSearch: _ext.type.search,
+ oSort: _ext.type.order,
+ afnSortData: _ext.order,
+ aoFeatures: _ext.feature,
+ oApi: _ext.internal,
+ oStdClasses: _ext.classes,
+ oPagination: _ext.pager
+ } );
+
+
+ $.extend( DataTable.ext.classes, {
+ "sTable": "dataTable",
+ "sNoFooter": "no-footer",
+
+ /* Paging buttons */
+ "sPageButton": "paginate_button",
+ "sPageButtonActive": "current",
+ "sPageButtonDisabled": "disabled",
+
+ /* Striping classes */
+ "sStripeOdd": "odd",
+ "sStripeEven": "even",
+
+ /* Empty row */
+ "sRowEmpty": "dataTables_empty",
+
+ /* Features */
+ "sWrapper": "dataTables_wrapper",
+ "sFilter": "dataTables_filter",
+ "sInfo": "dataTables_info",
+ "sPaging": "dataTables_paginate paging_", /* Note that the type is postfixed */
+ "sLength": "dataTables_length",
+ "sProcessing": "dataTables_processing",
+
+ /* Sorting */
+ "sSortAsc": "sorting_asc",
+ "sSortDesc": "sorting_desc",
+ "sSortable": "sorting", /* Sortable in both directions */
+ "sSortableAsc": "sorting_asc_disabled",
+ "sSortableDesc": "sorting_desc_disabled",
+ "sSortableNone": "sorting_disabled",
+ "sSortColumn": "sorting_", /* Note that an int is postfixed for the sorting order */
+
+ /* Filtering */
+ "sFilterInput": "",
+
+ /* Page length */
+ "sLengthSelect": "",
+
+ /* Scrolling */
+ "sScrollWrapper": "dataTables_scroll",
+ "sScrollHead": "dataTables_scrollHead",
+ "sScrollHeadInner": "dataTables_scrollHeadInner",
+ "sScrollBody": "dataTables_scrollBody",
+ "sScrollFoot": "dataTables_scrollFoot",
+ "sScrollFootInner": "dataTables_scrollFootInner",
+
+ /* Misc */
+ "sHeaderTH": "",
+ "sFooterTH": "",
+
+ // Deprecated
+ "sSortJUIAsc": "",
+ "sSortJUIDesc": "",
+ "sSortJUI": "",
+ "sSortJUIAscAllowed": "",
+ "sSortJUIDescAllowed": "",
+ "sSortJUIWrapper": "",
+ "sSortIcon": "",
+ "sJUIHeader": "",
+ "sJUIFooter": ""
+ } );
+
+
+ var extPagination = DataTable.ext.pager;
+
+ function _numbers ( page, pages ) {
+ var
+ numbers = [],
+ buttons = extPagination.numbers_length,
+ half = Math.floor( buttons / 2 ),
+ i = 1;
+
+ if ( pages <= buttons ) {
+ numbers = _range( 0, pages );
+ }
+ else if ( page <= half ) {
+ numbers = _range( 0, buttons-2 );
+ numbers.push( 'ellipsis' );
+ numbers.push( pages-1 );
+ }
+ else if ( page >= pages - 1 - half ) {
+ numbers = _range( pages-(buttons-2), pages );
+ numbers.splice( 0, 0, 'ellipsis' ); // no unshift in ie6
+ numbers.splice( 0, 0, 0 );
+ }
+ else {
+ numbers = _range( page-half+2, page+half-1 );
+ numbers.push( 'ellipsis' );
+ numbers.push( pages-1 );
+ numbers.splice( 0, 0, 'ellipsis' );
+ numbers.splice( 0, 0, 0 );
+ }
+
+ numbers.DT_el = 'span';
+ return numbers;
+ }
+
+
+ $.extend( extPagination, {
+ simple: function ( page, pages ) {
+ return [ 'previous', 'next' ];
+ },
+
+ full: function ( page, pages ) {
+ return [ 'first', 'previous', 'next', 'last' ];
+ },
+
+ numbers: function ( page, pages ) {
+ return [ _numbers(page, pages) ];
+ },
+
+ simple_numbers: function ( page, pages ) {
+ return [ 'previous', _numbers(page, pages), 'next' ];
+ },
+
+ full_numbers: function ( page, pages ) {
+ return [ 'first', 'previous', _numbers(page, pages), 'next', 'last' ];
+ },
+
+ first_last_numbers: function (page, pages) {
+ return ['first', _numbers(page, pages), 'last'];
+ },
+
+ // For testing and plug-ins to use
+ _numbers: _numbers,
+
+ // Number of number buttons (including ellipsis) to show. _Must be odd!_
+ numbers_length: 7
+ } );
+
+
+ $.extend( true, DataTable.ext.renderer, {
+ pageButton: {
+ _: function ( settings, host, idx, buttons, page, pages ) {
+ var classes = settings.oClasses;
+ var lang = settings.oLanguage.oPaginate;
+ var aria = settings.oLanguage.oAria.paginate || {};
+ var btnDisplay, btnClass, counter=0;
+
+ var attach = function( container, buttons ) {
+ var i, ien, node, button, tabIndex;
+ var disabledClass = classes.sPageButtonDisabled;
+ var clickHandler = function ( e ) {
+ _fnPageChange( settings, e.data.action, true );
+ };
+
+ for ( i=0, ien=buttons.length ; i ' )
+ .appendTo( container );
+ attach( inner, button );
+ }
+ else {
+ btnDisplay = null;
+ btnClass = button;
+ tabIndex = settings.iTabIndex;
+
+ switch ( button ) {
+ case 'ellipsis':
+ container.append('… ');
+ break;
+
+ case 'first':
+ btnDisplay = lang.sFirst;
+
+ if ( page === 0 ) {
+ tabIndex = -1;
+ btnClass += ' ' + disabledClass;
+ }
+ break;
+
+ case 'previous':
+ btnDisplay = lang.sPrevious;
+
+ if ( page === 0 ) {
+ tabIndex = -1;
+ btnClass += ' ' + disabledClass;
+ }
+ break;
+
+ case 'next':
+ btnDisplay = lang.sNext;
+
+ if ( page === pages-1 ) {
+ tabIndex = -1;
+ btnClass += ' ' + disabledClass;
+ }
+ break;
+
+ case 'last':
+ btnDisplay = lang.sLast;
+
+ if ( page === pages-1 ) {
+ tabIndex = -1;
+ btnClass += ' ' + disabledClass;
+ }
+ break;
+
+ default:
+ btnDisplay = button + 1;
+ btnClass = page === button ?
+ classes.sPageButtonActive : '';
+ break;
+ }
+
+ if ( btnDisplay !== null ) {
+ node = $('', {
+ 'class': classes.sPageButton+' '+btnClass,
+ 'aria-controls': settings.sTableId,
+ 'aria-label': aria[ button ],
+ 'data-dt-idx': counter,
+ 'tabindex': tabIndex,
+ 'id': idx === 0 && typeof button === 'string' ?
+ settings.sTableId +'_'+ button :
+ null
+ } )
+ .html( btnDisplay )
+ .appendTo( container );
+
+ _fnBindAction(
+ node, {action: button}, clickHandler
+ );
+
+ counter++;
+ }
+ }
+ }
+ };
+
+ // IE9 throws an 'unknown error' if document.activeElement is used
+ // inside an iframe or frame. Try / catch the error. Not good for
+ // accessibility, but neither are frames.
+ var activeEl;
+
+ try {
+ // Because this approach is destroying and recreating the paging
+ // elements, focus is lost on the select button which is bad for
+ // accessibility. So we want to restore focus once the draw has
+ // completed
+ activeEl = $(host).find(document.activeElement).data('dt-idx');
+ }
+ catch (e) {}
+
+ attach( $(host).empty(), buttons );
+
+ if ( activeEl !== undefined ) {
+ $(host).find( '[data-dt-idx='+activeEl+']' ).focus();
+ }
+ }
+ }
+ } );
+
+
+
+ // Built in type detection. See model.ext.aTypes for information about
+ // what is required from this methods.
+ $.extend( DataTable.ext.type.detect, [
+ // Plain numbers - first since V8 detects some plain numbers as dates
+ // e.g. Date.parse('55') (but not all, e.g. Date.parse('22')...).
+ function ( d, settings )
+ {
+ var decimal = settings.oLanguage.sDecimal;
+ return _isNumber( d, decimal ) ? 'num'+decimal : null;
+ },
+
+ // Dates (only those recognised by the browser's Date.parse)
+ function ( d, settings )
+ {
+ // V8 tries _very_ hard to make a string passed into `Date.parse()`
+ // valid, so we need to use a regex to restrict date formats. Use a
+ // plug-in for anything other than ISO8601 style strings
+ if ( d && !(d instanceof Date) && ! _re_date.test(d) ) {
+ return null;
+ }
+ var parsed = Date.parse(d);
+ return (parsed !== null && !isNaN(parsed)) || _empty(d) ? 'date' : null;
+ },
+
+ // Formatted numbers
+ function ( d, settings )
+ {
+ var decimal = settings.oLanguage.sDecimal;
+ return _isNumber( d, decimal, true ) ? 'num-fmt'+decimal : null;
+ },
+
+ // HTML numeric
+ function ( d, settings )
+ {
+ var decimal = settings.oLanguage.sDecimal;
+ return _htmlNumeric( d, decimal ) ? 'html-num'+decimal : null;
+ },
+
+ // HTML numeric, formatted
+ function ( d, settings )
+ {
+ var decimal = settings.oLanguage.sDecimal;
+ return _htmlNumeric( d, decimal, true ) ? 'html-num-fmt'+decimal : null;
+ },
+
+ // HTML (this is strict checking - there must be html)
+ function ( d, settings )
+ {
+ return _empty( d ) || (typeof d === 'string' && d.indexOf('<') !== -1) ?
+ 'html' : null;
+ }
+ ] );
+
+
+
+ // Filter formatting functions. See model.ext.ofnSearch for information about
+ // what is required from these methods.
+ //
+ // Note that additional search methods are added for the html numbers and
+ // html formatted numbers by `_addNumericSort()` when we know what the decimal
+ // place is
+
+
+ $.extend( DataTable.ext.type.search, {
+ html: function ( data ) {
+ return _empty(data) ?
+ data :
+ typeof data === 'string' ?
+ data
+ .replace( _re_new_lines, " " )
+ .replace( _re_html, "" ) :
+ '';
+ },
+
+ string: function ( data ) {
+ return _empty(data) ?
+ data :
+ typeof data === 'string' ?
+ data.replace( _re_new_lines, " " ) :
+ data;
+ }
+ } );
+
+
+
+ var __numericReplace = function ( d, decimalPlace, re1, re2 ) {
+ if ( d !== 0 && (!d || d === '-') ) {
+ return -Infinity;
+ }
+
+ // If a decimal place other than `.` is used, it needs to be given to the
+ // function so we can detect it and replace with a `.` which is the only
+ // decimal place Javascript recognises - it is not locale aware.
+ if ( decimalPlace ) {
+ d = _numToDecimal( d, decimalPlace );
+ }
+
+ if ( d.replace ) {
+ if ( re1 ) {
+ d = d.replace( re1, '' );
+ }
+
+ if ( re2 ) {
+ d = d.replace( re2, '' );
+ }
+ }
+
+ return d * 1;
+ };
+
+
+ // Add the numeric 'deformatting' functions for sorting and search. This is done
+ // in a function to provide an easy ability for the language options to add
+ // additional methods if a non-period decimal place is used.
+ function _addNumericSort ( decimalPlace ) {
+ $.each(
+ {
+ // Plain numbers
+ "num": function ( d ) {
+ return __numericReplace( d, decimalPlace );
+ },
+
+ // Formatted numbers
+ "num-fmt": function ( d ) {
+ return __numericReplace( d, decimalPlace, _re_formatted_numeric );
+ },
+
+ // HTML numeric
+ "html-num": function ( d ) {
+ return __numericReplace( d, decimalPlace, _re_html );
+ },
+
+ // HTML numeric, formatted
+ "html-num-fmt": function ( d ) {
+ return __numericReplace( d, decimalPlace, _re_html, _re_formatted_numeric );
+ }
+ },
+ function ( key, fn ) {
+ // Add the ordering method
+ _ext.type.order[ key+decimalPlace+'-pre' ] = fn;
+
+ // For HTML types add a search formatter that will strip the HTML
+ if ( key.match(/^html\-/) ) {
+ _ext.type.search[ key+decimalPlace ] = _ext.type.search.html;
+ }
+ }
+ );
+ }
+
+
+ // Default sort methods
+ $.extend( _ext.type.order, {
+ // Dates
+ "date-pre": function ( d ) {
+ var ts = Date.parse( d );
+ return isNaN(ts) ? -Infinity : ts;
+ },
+
+ // html
+ "html-pre": function ( a ) {
+ return _empty(a) ?
+ '' :
+ a.replace ?
+ a.replace( /<.*?>/g, "" ).toLowerCase() :
+ a+'';
+ },
+
+ // string
+ "string-pre": function ( a ) {
+ // This is a little complex, but faster than always calling toString,
+ // http://jsperf.com/tostring-v-check
+ return _empty(a) ?
+ '' :
+ typeof a === 'string' ?
+ a.toLowerCase() :
+ ! a.toString ?
+ '' :
+ a.toString();
+ },
+
+ // string-asc and -desc are retained only for compatibility with the old
+ // sort methods
+ "string-asc": function ( x, y ) {
+ return ((x < y) ? -1 : ((x > y) ? 1 : 0));
+ },
+
+ "string-desc": function ( x, y ) {
+ return ((x < y) ? 1 : ((x > y) ? -1 : 0));
+ }
+ } );
+
+
+ // Numeric sorting types - order doesn't matter here
+ _addNumericSort( '' );
+
+
+ $.extend( true, DataTable.ext.renderer, {
+ header: {
+ _: function ( settings, cell, column, classes ) {
+ // No additional mark-up required
+ // Attach a sort listener to update on sort - note that using the
+ // `DT` namespace will allow the event to be removed automatically
+ // on destroy, while the `dt` namespaced event is the one we are
+ // listening for
+ $(settings.nTable).on( 'order.dt.DT', function ( e, ctx, sorting, columns ) {
+ if ( settings !== ctx ) { // need to check this this is the host
+ return; // table, not a nested one
+ }
+
+ var colIdx = column.idx;
+
+ cell
+ .removeClass(
+ column.sSortingClass +' '+
+ classes.sSortAsc +' '+
+ classes.sSortDesc
+ )
+ .addClass( columns[ colIdx ] == 'asc' ?
+ classes.sSortAsc : columns[ colIdx ] == 'desc' ?
+ classes.sSortDesc :
+ column.sSortingClass
+ );
+ } );
+ },
+
+ jqueryui: function ( settings, cell, column, classes ) {
+ $('
')
+ .addClass( classes.sSortJUIWrapper )
+ .append( cell.contents() )
+ .append( $(' ')
+ .addClass( classes.sSortIcon+' '+column.sSortingClassJUI )
+ )
+ .appendTo( cell );
+
+ // Attach a sort listener to update on sort
+ $(settings.nTable).on( 'order.dt.DT', function ( e, ctx, sorting, columns ) {
+ if ( settings !== ctx ) {
+ return;
+ }
+
+ var colIdx = column.idx;
+
+ cell
+ .removeClass( classes.sSortAsc +" "+classes.sSortDesc )
+ .addClass( columns[ colIdx ] == 'asc' ?
+ classes.sSortAsc : columns[ colIdx ] == 'desc' ?
+ classes.sSortDesc :
+ column.sSortingClass
+ );
+
+ cell
+ .find( 'span.'+classes.sSortIcon )
+ .removeClass(
+ classes.sSortJUIAsc +" "+
+ classes.sSortJUIDesc +" "+
+ classes.sSortJUI +" "+
+ classes.sSortJUIAscAllowed +" "+
+ classes.sSortJUIDescAllowed
+ )
+ .addClass( columns[ colIdx ] == 'asc' ?
+ classes.sSortJUIAsc : columns[ colIdx ] == 'desc' ?
+ classes.sSortJUIDesc :
+ column.sSortingClassJUI
+ );
+ } );
+ }
+ }
+ } );
+
+ /*
+ * Public helper functions. These aren't used internally by DataTables, or
+ * called by any of the options passed into DataTables, but they can be used
+ * externally by developers working with DataTables. They are helper functions
+ * to make working with DataTables a little bit easier.
+ */
+
+ var __htmlEscapeEntities = function ( d ) {
+ return typeof d === 'string' ?
+ d.replace(//g, '>').replace(/"/g, '"') :
+ d;
+ };
+
+ /**
+ * Helpers for `columns.render`.
+ *
+ * The options defined here can be used with the `columns.render` initialisation
+ * option to provide a display renderer. The following functions are defined:
+ *
+ * * `number` - Will format numeric data (defined by `columns.data`) for
+ * display, retaining the original unformatted data for sorting and filtering.
+ * It takes 5 parameters:
+ * * `string` - Thousands grouping separator
+ * * `string` - Decimal point indicator
+ * * `integer` - Number of decimal points to show
+ * * `string` (optional) - Prefix.
+ * * `string` (optional) - Postfix (/suffix).
+ * * `text` - Escape HTML to help prevent XSS attacks. It has no optional
+ * parameters.
+ *
+ * @example
+ * // Column definition using the number renderer
+ * {
+ * data: "salary",
+ * render: $.fn.dataTable.render.number( '\'', '.', 0, '$' )
+ * }
+ *
+ * @namespace
+ */
+ DataTable.render = {
+ number: function ( thousands, decimal, precision, prefix, postfix ) {
+ return {
+ display: function ( d ) {
+ if ( typeof d !== 'number' && typeof d !== 'string' ) {
+ return d;
+ }
+
+ var negative = d < 0 ? '-' : '';
+ var flo = parseFloat( d );
+
+ // If NaN then there isn't much formatting that we can do - just
+ // return immediately, escaping any HTML (this was supposed to
+ // be a number after all)
+ if ( isNaN( flo ) ) {
+ return __htmlEscapeEntities( d );
+ }
+
+ flo = flo.toFixed( precision );
+ d = Math.abs( flo );
+
+ var intPart = parseInt( d, 10 );
+ var floatPart = precision ?
+ decimal+(d - intPart).toFixed( precision ).substring( 2 ):
+ '';
+
+ return negative + (prefix||'') +
+ intPart.toString().replace(
+ /\B(?=(\d{3})+(?!\d))/g, thousands
+ ) +
+ floatPart +
+ (postfix||'');
+ }
+ };
+ },
+
+ text: function () {
+ return {
+ display: __htmlEscapeEntities,
+ filter: __htmlEscapeEntities
+ };
+ }
+ };
+
+
+ /*
+ * This is really a good bit rubbish this method of exposing the internal methods
+ * publicly... - To be fixed in 2.0 using methods on the prototype
+ */
+
+
+ /**
+ * Create a wrapper function for exporting an internal functions to an external API.
+ * @param {string} fn API function name
+ * @returns {function} wrapped function
+ * @memberof DataTable#internal
+ */
+ function _fnExternApiFunc (fn)
+ {
+ return function() {
+ var args = [_fnSettingsFromNode( this[DataTable.ext.iApiIndex] )].concat(
+ Array.prototype.slice.call(arguments)
+ );
+ return DataTable.ext.internal[fn].apply( this, args );
+ };
+ }
+
+
+ /**
+ * Reference to internal functions for use by plug-in developers. Note that
+ * these methods are references to internal functions and are considered to be
+ * private. If you use these methods, be aware that they are liable to change
+ * between versions.
+ * @namespace
+ */
+ $.extend( DataTable.ext.internal, {
+ _fnExternApiFunc: _fnExternApiFunc,
+ _fnBuildAjax: _fnBuildAjax,
+ _fnAjaxUpdate: _fnAjaxUpdate,
+ _fnAjaxParameters: _fnAjaxParameters,
+ _fnAjaxUpdateDraw: _fnAjaxUpdateDraw,
+ _fnAjaxDataSrc: _fnAjaxDataSrc,
+ _fnAddColumn: _fnAddColumn,
+ _fnColumnOptions: _fnColumnOptions,
+ _fnAdjustColumnSizing: _fnAdjustColumnSizing,
+ _fnVisibleToColumnIndex: _fnVisibleToColumnIndex,
+ _fnColumnIndexToVisible: _fnColumnIndexToVisible,
+ _fnVisbleColumns: _fnVisbleColumns,
+ _fnGetColumns: _fnGetColumns,
+ _fnColumnTypes: _fnColumnTypes,
+ _fnApplyColumnDefs: _fnApplyColumnDefs,
+ _fnHungarianMap: _fnHungarianMap,
+ _fnCamelToHungarian: _fnCamelToHungarian,
+ _fnLanguageCompat: _fnLanguageCompat,
+ _fnBrowserDetect: _fnBrowserDetect,
+ _fnAddData: _fnAddData,
+ _fnAddTr: _fnAddTr,
+ _fnNodeToDataIndex: _fnNodeToDataIndex,
+ _fnNodeToColumnIndex: _fnNodeToColumnIndex,
+ _fnGetCellData: _fnGetCellData,
+ _fnSetCellData: _fnSetCellData,
+ _fnSplitObjNotation: _fnSplitObjNotation,
+ _fnGetObjectDataFn: _fnGetObjectDataFn,
+ _fnSetObjectDataFn: _fnSetObjectDataFn,
+ _fnGetDataMaster: _fnGetDataMaster,
+ _fnClearTable: _fnClearTable,
+ _fnDeleteIndex: _fnDeleteIndex,
+ _fnInvalidate: _fnInvalidate,
+ _fnGetRowElements: _fnGetRowElements,
+ _fnCreateTr: _fnCreateTr,
+ _fnBuildHead: _fnBuildHead,
+ _fnDrawHead: _fnDrawHead,
+ _fnDraw: _fnDraw,
+ _fnReDraw: _fnReDraw,
+ _fnAddOptionsHtml: _fnAddOptionsHtml,
+ _fnDetectHeader: _fnDetectHeader,
+ _fnGetUniqueThs: _fnGetUniqueThs,
+ _fnFeatureHtmlFilter: _fnFeatureHtmlFilter,
+ _fnFilterComplete: _fnFilterComplete,
+ _fnFilterCustom: _fnFilterCustom,
+ _fnFilterColumn: _fnFilterColumn,
+ _fnFilter: _fnFilter,
+ _fnFilterCreateSearch: _fnFilterCreateSearch,
+ _fnEscapeRegex: _fnEscapeRegex,
+ _fnFilterData: _fnFilterData,
+ _fnFeatureHtmlInfo: _fnFeatureHtmlInfo,
+ _fnUpdateInfo: _fnUpdateInfo,
+ _fnInfoMacros: _fnInfoMacros,
+ _fnInitialise: _fnInitialise,
+ _fnInitComplete: _fnInitComplete,
+ _fnLengthChange: _fnLengthChange,
+ _fnFeatureHtmlLength: _fnFeatureHtmlLength,
+ _fnFeatureHtmlPaginate: _fnFeatureHtmlPaginate,
+ _fnPageChange: _fnPageChange,
+ _fnFeatureHtmlProcessing: _fnFeatureHtmlProcessing,
+ _fnProcessingDisplay: _fnProcessingDisplay,
+ _fnFeatureHtmlTable: _fnFeatureHtmlTable,
+ _fnScrollDraw: _fnScrollDraw,
+ _fnApplyToChildren: _fnApplyToChildren,
+ _fnCalculateColumnWidths: _fnCalculateColumnWidths,
+ _fnThrottle: _fnThrottle,
+ _fnConvertToWidth: _fnConvertToWidth,
+ _fnGetWidestNode: _fnGetWidestNode,
+ _fnGetMaxLenString: _fnGetMaxLenString,
+ _fnStringToCss: _fnStringToCss,
+ _fnSortFlatten: _fnSortFlatten,
+ _fnSort: _fnSort,
+ _fnSortAria: _fnSortAria,
+ _fnSortListener: _fnSortListener,
+ _fnSortAttachListener: _fnSortAttachListener,
+ _fnSortingClasses: _fnSortingClasses,
+ _fnSortData: _fnSortData,
+ _fnSaveState: _fnSaveState,
+ _fnLoadState: _fnLoadState,
+ _fnSettingsFromNode: _fnSettingsFromNode,
+ _fnLog: _fnLog,
+ _fnMap: _fnMap,
+ _fnBindAction: _fnBindAction,
+ _fnCallbackReg: _fnCallbackReg,
+ _fnCallbackFire: _fnCallbackFire,
+ _fnLengthOverflow: _fnLengthOverflow,
+ _fnRenderer: _fnRenderer,
+ _fnDataSource: _fnDataSource,
+ _fnRowAttributes: _fnRowAttributes,
+ _fnExtend: _fnExtend,
+ _fnCalculateEnd: function () {} // Used by a lot of plug-ins, but redundant
+ // in 1.10, so this dead-end function is
+ // added to prevent errors
+ } );
+
+
+ // jQuery access
+ $.fn.dataTable = DataTable;
+
+ // Provide access to the host jQuery object (circular reference)
+ DataTable.$ = $;
+
+ // Legacy aliases
+ $.fn.dataTableSettings = DataTable.settings;
+ $.fn.dataTableExt = DataTable.ext;
+
+ // With a capital `D` we return a DataTables API instance rather than a
+ // jQuery object
+ $.fn.DataTable = function ( opts ) {
+ return $(this).dataTable( opts ).api();
+ };
+
+ // All properties that are available to $.fn.dataTable should also be
+ // available on $.fn.DataTable
+ $.each( DataTable, function ( prop, val ) {
+ $.fn.DataTable[ prop ] = val;
+ } );
+
+
+ // Information about events fired by DataTables - for documentation.
+ /**
+ * Draw event, fired whenever the table is redrawn on the page, at the same
+ * point as fnDrawCallback. This may be useful for binding events or
+ * performing calculations when the table is altered at all.
+ * @name DataTable#draw.dt
+ * @event
+ * @param {event} e jQuery event object
+ * @param {object} o DataTables settings object {@link DataTable.models.oSettings}
+ */
+
+ /**
+ * Search event, fired when the searching applied to the table (using the
+ * built-in global search, or column filters) is altered.
+ * @name DataTable#search.dt
+ * @event
+ * @param {event} e jQuery event object
+ * @param {object} o DataTables settings object {@link DataTable.models.oSettings}
+ */
+
+ /**
+ * Page change event, fired when the paging of the table is altered.
+ * @name DataTable#page.dt
+ * @event
+ * @param {event} e jQuery event object
+ * @param {object} o DataTables settings object {@link DataTable.models.oSettings}
+ */
+
+ /**
+ * Order event, fired when the ordering applied to the table is altered.
+ * @name DataTable#order.dt
+ * @event
+ * @param {event} e jQuery event object
+ * @param {object} o DataTables settings object {@link DataTable.models.oSettings}
+ */
+
+ /**
+ * DataTables initialisation complete event, fired when the table is fully
+ * drawn, including Ajax data loaded, if Ajax data is required.
+ * @name DataTable#init.dt
+ * @event
+ * @param {event} e jQuery event object
+ * @param {object} oSettings DataTables settings object
+ * @param {object} json The JSON object request from the server - only
+ * present if client-side Ajax sourced data is used
+ */
+
+ /**
+ * State save event, fired when the table has changed state a new state save
+ * is required. This event allows modification of the state saving object
+ * prior to actually doing the save, including addition or other state
+ * properties (for plug-ins) or modification of a DataTables core property.
+ * @name DataTable#stateSaveParams.dt
+ * @event
+ * @param {event} e jQuery event object
+ * @param {object} oSettings DataTables settings object
+ * @param {object} json The state information to be saved
+ */
+
+ /**
+ * State load event, fired when the table is loading state from the stored
+ * data, but prior to the settings object being modified by the saved state
+ * - allowing modification of the saved state is required or loading of
+ * state for a plug-in.
+ * @name DataTable#stateLoadParams.dt
+ * @event
+ * @param {event} e jQuery event object
+ * @param {object} oSettings DataTables settings object
+ * @param {object} json The saved state information
+ */
+
+ /**
+ * State loaded event, fired when state has been loaded from stored data and
+ * the settings object has been modified by the loaded data.
+ * @name DataTable#stateLoaded.dt
+ * @event
+ * @param {event} e jQuery event object
+ * @param {object} oSettings DataTables settings object
+ * @param {object} json The saved state information
+ */
+
+ /**
+ * Processing event, fired when DataTables is doing some kind of processing
+ * (be it, order, search or anything else). It can be used to indicate to
+ * the end user that there is something happening, or that something has
+ * finished.
+ * @name DataTable#processing.dt
+ * @event
+ * @param {event} e jQuery event object
+ * @param {object} oSettings DataTables settings object
+ * @param {boolean} bShow Flag for if DataTables is doing processing or not
+ */
+
+ /**
+ * Ajax (XHR) event, fired whenever an Ajax request is completed from a
+ * request to made to the server for new data. This event is called before
+ * DataTables processed the returned data, so it can also be used to pre-
+ * process the data returned from the server, if needed.
+ *
+ * Note that this trigger is called in `fnServerData`, if you override
+ * `fnServerData` and which to use this event, you need to trigger it in you
+ * success function.
+ * @name DataTable#xhr.dt
+ * @event
+ * @param {event} e jQuery event object
+ * @param {object} o DataTables settings object {@link DataTable.models.oSettings}
+ * @param {object} json JSON returned from the server
+ *
+ * @example
+ * // Use a custom property returned from the server in another DOM element
+ * $('#table').dataTable().on('xhr.dt', function (e, settings, json) {
+ * $('#status').html( json.status );
+ * } );
+ *
+ * @example
+ * // Pre-process the data returned from the server
+ * $('#table').dataTable().on('xhr.dt', function (e, settings, json) {
+ * for ( var i=0, ien=json.aaData.length ; i").css({position:"fixed",top:0,left:-1*h(E).scrollLeft(),height:1,width:1,
+overflow:"hidden"}).append(h("
").css({position:"absolute",top:1,left:1,width:100,overflow:"scroll"}).append(h("
").css({width:"100%",height:10}))).appendTo("body"),d=c.children(),e=d.children();b.barWidth=d[0].offsetWidth-d[0].clientWidth;b.bScrollOversize=100===e[0].offsetWidth&&100!==d[0].clientWidth;b.bScrollbarLeft=1!==Math.round(e.offset().left);b.bBounding=c[0].getBoundingClientRect().width?!0:!1;c.remove()}h.extend(a.oBrowser,n.__browser);a.oScroll.iBarWidth=n.__browser.barWidth}
+function jb(a,b,c,d,e,f){var g,j=!1;c!==k&&(g=c,j=!0);for(;d!==e;)a.hasOwnProperty(d)&&(g=j?b(g,a[d],d,a):a[d],j=!0,d+=f);return g}function Ga(a,b){var c=n.defaults.column,d=a.aoColumns.length,c=h.extend({},n.models.oColumn,c,{nTh:b?b:H.createElement("th"),sTitle:c.sTitle?c.sTitle:b?b.innerHTML:"",aDataSort:c.aDataSort?c.aDataSort:[d],mData:c.mData?c.mData:d,idx:d});a.aoColumns.push(c);c=a.aoPreSearchCols;c[d]=h.extend({},n.models.oSearch,c[d]);la(a,d,h(b).data())}function la(a,b,c){var b=a.aoColumns[b],
+d=a.oClasses,e=h(b.nTh);if(!b.sWidthOrig){b.sWidthOrig=e.attr("width")||null;var f=(e.attr("style")||"").match(/width:\s*(\d+[pxem%]+)/);f&&(b.sWidthOrig=f[1])}c!==k&&null!==c&&(hb(c),J(n.defaults.column,c,!0),c.mDataProp!==k&&!c.mData&&(c.mData=c.mDataProp),c.sType&&(b._sManualType=c.sType),c.className&&!c.sClass&&(c.sClass=c.className),c.sClass&&e.addClass(c.sClass),h.extend(b,c),F(b,c,"sWidth","sWidthOrig"),c.iDataSort!==k&&(b.aDataSort=[c.iDataSort]),F(b,c,"aDataSort"));var g=b.mData,j=S(g),i=
+b.mRender?S(b.mRender):null,c=function(a){return"string"===typeof a&&-1!==a.indexOf("@")};b._bAttrSrc=h.isPlainObject(g)&&(c(g.sort)||c(g.type)||c(g.filter));b._setter=null;b.fnGetData=function(a,b,c){var d=j(a,b,k,c);return i&&b?i(d,b,a,c):d};b.fnSetData=function(a,b,c){return N(g)(a,b,c)};"number"!==typeof g&&(a._rowReadObject=!0);a.oFeatures.bSort||(b.bSortable=!1,e.addClass(d.sSortableNone));a=-1!==h.inArray("asc",b.asSorting);c=-1!==h.inArray("desc",b.asSorting);!b.bSortable||!a&&!c?(b.sSortingClass=
+d.sSortableNone,b.sSortingClassJUI=""):a&&!c?(b.sSortingClass=d.sSortableAsc,b.sSortingClassJUI=d.sSortJUIAscAllowed):!a&&c?(b.sSortingClass=d.sSortableDesc,b.sSortingClassJUI=d.sSortJUIDescAllowed):(b.sSortingClass=d.sSortable,b.sSortingClassJUI=d.sSortJUI)}function aa(a){if(!1!==a.oFeatures.bAutoWidth){var b=a.aoColumns;Ha(a);for(var c=0,d=b.length;cq[f])d(m.length+
+q[f],l);else if("string"===typeof q[f]){j=0;for(i=m.length;jb&&a[e]--; -1!=d&&c===k&&a.splice(d,1)}function ea(a,b,c,d){var e=a.aoData[b],f,g=function(c,d){for(;c.childNodes.length;)c.removeChild(c.firstChild);c.innerHTML=B(a,b,d,"display")};if("dom"===c||(!c||"auto"===c)&&"dom"===e.src)e._aData=Ka(a,e,d,d===k?k:e._aData).data;else{var j=e.anCells;if(j)if(d!==k)g(j[d],d);else{c=0;for(f=j.length;c ").appendTo(g));b=0;for(c=m.length;btr").attr("role","row");h(g).find(">tr>th, >tr>td").addClass(l.sHeaderTH);h(j).find(">tr>th, >tr>td").addClass(l.sFooterTH);if(null!==j){a=a.aoFooter[0];b=0;for(c=a.length;b=a.fnRecordsDisplay()?0:g,a.iInitDisplayStart=-1);var g=a._iDisplayStart,l=a.fnDisplayEnd();if(a.bDeferLoading)a.bDeferLoading=!1,a.iDraw++,D(a,!1);else if(j){if(!a.bDestroying&&!nb(a))return}else a.iDraw++;if(0!==i.length){f=j?a.aoData.length:l;for(j=j?0:g;j",{"class":e?d[0]:""}).append(h(" ",{valign:"top",colSpan:W(a),"class":a.oClasses.sRowEmpty}).html(c))[0];t(a,"aoHeaderCallback","header",[h(a.nTHead).children("tr")[0],Ma(a),g,l,i]);t(a,"aoFooterCallback","footer",[h(a.nTFoot).children("tr")[0],
+Ma(a),g,l,i]);d=h(a.nTBody);d.children().detach();d.append(h(b));t(a,"aoDrawCallback","draw",[a]);a.bSorted=!1;a.bFiltered=!1;a.bDrawing=!1}}function T(a,b){var c=a.oFeatures,d=c.bFilter;c.bSort&&ob(a);d?ha(a,a.oPreviousSearch):a.aiDisplay=a.aiDisplayMaster.slice();!0!==b&&(a._iDisplayStart=0);a._drawHold=b;P(a);a._drawHold=!1}function pb(a){var b=a.oClasses,c=h(a.nTable),c=h("
").insertBefore(c),d=a.oFeatures,e=h("
",{id:a.sTableId+"_wrapper","class":b.sWrapper+(a.nTFoot?"":" "+b.sNoFooter)});
+a.nHolding=c[0];a.nTableWrapper=e[0];a.nTableReinsertBefore=a.nTable.nextSibling;for(var f=a.sDom.split(""),g,j,i,l,m,q,k=0;k ")[0];l=f[k+1];if("'"==l||'"'==l){m="";for(q=2;f[k+q]!=l;)m+=f[k+q],q++;"H"==m?m=b.sJUIHeader:"F"==m&&(m=b.sJUIFooter);-1!=m.indexOf(".")?(l=m.split("."),i.id=l[0].substr(1,l[0].length-1),i.className=l[1]):"#"==m.charAt(0)?i.id=m.substr(1,m.length-1):i.className=m;k+=q}e.append(i);e=h(i)}else if(">"==j)e=e.parent();else if("l"==
+j&&d.bPaginate&&d.bLengthChange)g=qb(a);else if("f"==j&&d.bFilter)g=rb(a);else if("r"==j&&d.bProcessing)g=sb(a);else if("t"==j)g=tb(a);else if("i"==j&&d.bInfo)g=ub(a);else if("p"==j&&d.bPaginate)g=vb(a);else if(0!==n.ext.feature.length){i=n.ext.feature;q=0;for(l=i.length;q ',j=d.sSearch,j=j.match(/_INPUT_/)?j.replace("_INPUT_",g):j+g,b=h("
",{id:!f.f?c+"_filter":null,"class":b.sFilter}).append(h(" ").append(j)),f=function(){var b=!this.value?"":this.value;b!=e.sSearch&&(ha(a,{sSearch:b,bRegex:e.bRegex,bSmart:e.bSmart,bCaseInsensitive:e.bCaseInsensitive}),a._iDisplayStart=0,P(a))},g=null!==a.searchDelay?a.searchDelay:"ssp"===y(a)?400:0,i=h("input",b).val(e.sSearch).attr("placeholder",d.sSearchPlaceholder).on("keyup.DT search.DT input.DT paste.DT cut.DT",
+g?Qa(f,g):f).on("keypress.DT",function(a){if(13==a.keyCode)return!1}).attr("aria-controls",c);h(a.nTable).on("search.dt.DT",function(b,c){if(a===c)try{i[0]!==H.activeElement&&i.val(e.sSearch)}catch(d){}});return b[0]}function ha(a,b,c){var d=a.oPreviousSearch,e=a.aoPreSearchCols,f=function(a){d.sSearch=a.sSearch;d.bRegex=a.bRegex;d.bSmart=a.bSmart;d.bCaseInsensitive=a.bCaseInsensitive};Ia(a);if("ssp"!=y(a)){yb(a,b.sSearch,c,b.bEscapeRegex!==k?!b.bEscapeRegex:b.bRegex,b.bSmart,b.bCaseInsensitive);
+f(b);for(b=0;b=b.length)a.aiDisplay=j.slice();else{if(i||c||d||g.length>b.length||0!==b.indexOf(g)||a.bSorted)a.aiDisplay=j.slice();b=a.aiDisplay;for(c=0;c",{"class":a.oClasses.sInfo,id:!c?b+"_info":null});c||(a.aoDrawCallback.push({fn:Eb,sName:"information"}),
+d.attr("role","status").attr("aria-live","polite"),h(a.nTable).attr("aria-describedby",b+"_info"));return d[0]}function Eb(a){var b=a.aanFeatures.i;if(0!==b.length){var c=a.oLanguage,d=a._iDisplayStart+1,e=a.fnDisplayEnd(),f=a.fnRecordsTotal(),g=a.fnRecordsDisplay(),j=g?c.sInfo:c.sInfoEmpty;g!==f&&(j+=" "+c.sInfoFiltered);j+=c.sInfoPostFix;j=Fb(a,j);c=c.fnInfoCallback;null!==c&&(j=c.call(a.oInstance,a,d,e,f,g,j));h(b).html(j)}}function Fb(a,b){var c=a.fnFormatNumber,d=a._iDisplayStart+1,e=a._iDisplayLength,
+f=a.fnRecordsDisplay(),g=-1===e;return b.replace(/_START_/g,c.call(a,d)).replace(/_END_/g,c.call(a,a.fnDisplayEnd())).replace(/_MAX_/g,c.call(a,a.fnRecordsTotal())).replace(/_TOTAL_/g,c.call(a,f)).replace(/_PAGE_/g,c.call(a,g?1:Math.ceil(d/e))).replace(/_PAGES_/g,c.call(a,g?1:Math.ceil(f/e)))}function ia(a){var b,c,d=a.iInitDisplayStart,e=a.aoColumns,f;c=a.oFeatures;var g=a.bDeferLoading;if(a.bInitialised){pb(a);mb(a);ga(a,a.aoHeader);ga(a,a.aoFooter);D(a,!0);c.bAutoWidth&&Ha(a);b=0;for(c=e.length;b<
+c;b++)f=e[b],f.sWidth&&(f.nTh.style.width=w(f.sWidth));t(a,null,"preInit",[a]);T(a);e=y(a);if("ssp"!=e||g)"ajax"==e?ta(a,[],function(c){var f=ua(a,c);for(b=0;b",{name:c+"_length","aria-controls":c,"class":b.sLengthSelect}),g=0,j=f.length;g ").addClass(b.sLength);a.aanFeatures.l||(i[0].id=c+"_length");i.children().append(a.oLanguage.sLengthMenu.replace("_MENU_",e[0].outerHTML));h("select",i).val(a._iDisplayLength).on("change.DT",
+function(){Ta(a,h(this).val());P(a)});h(a.nTable).on("length.dt.DT",function(b,c,d){a===c&&h("select",i).val(d)});return i[0]}function vb(a){var b=a.sPaginationType,c=n.ext.pager[b],d="function"===typeof c,e=function(a){P(a)},b=h("
").addClass(a.oClasses.sPaging+b)[0],f=a.aanFeatures;d||c.fnInit(a,b,e);f.p||(b.id=a.sTableId+"_paginate",a.aoDrawCallback.push({fn:function(a){if(d){var b=a._iDisplayStart,i=a._iDisplayLength,h=a.fnRecordsDisplay(),m=-1===i,b=m?0:Math.ceil(b/i),i=m?1:Math.ceil(h/
+i),h=c(b,i),k,m=0;for(k=f.p.length;mf&&(d=0)):"first"==b?d=0:"previous"==b?(d=0<=e?d-e:0,0>d&&(d=0)):"next"==b?d+e ",{id:!a.aanFeatures.r?a.sTableId+"_processing":null,"class":a.oClasses.sProcessing}).html(a.oLanguage.sProcessing).insertBefore(a.nTable)[0]}function D(a,b){a.oFeatures.bProcessing&&h(a.aanFeatures.r).css("display",b?"block":"none");t(a,null,"processing",[a,b])}function tb(a){var b=h(a.nTable);b.attr("role","grid");var c=a.oScroll;if(""===c.sX&&""===c.sY)return a.nTable;var d=c.sX,e=c.sY,f=a.oClasses,g=b.children("caption"),j=g.length?g[0]._captionSide:null,
+i=h(b[0].cloneNode(!1)),l=h(b[0].cloneNode(!1)),m=b.children("tfoot");m.length||(m=null);i=h("
",{"class":f.sScrollWrapper}).append(h("
",{"class":f.sScrollHead}).css({overflow:"hidden",position:"relative",border:0,width:d?!d?null:w(d):"100%"}).append(h("
",{"class":f.sScrollHeadInner}).css({"box-sizing":"content-box",width:c.sXInner||"100%"}).append(i.removeAttr("id").css("margin-left",0).append("top"===j?g:null).append(b.children("thead"))))).append(h("
",{"class":f.sScrollBody}).css({position:"relative",
+overflow:"auto",width:!d?null:w(d)}).append(b));m&&i.append(h("
",{"class":f.sScrollFoot}).css({overflow:"hidden",border:0,width:d?!d?null:w(d):"100%"}).append(h("
",{"class":f.sScrollFootInner}).append(l.removeAttr("id").css("margin-left",0).append("bottom"===j?g:null).append(b.children("tfoot")))));var b=i.children(),k=b[0],f=b[1],u=m?b[2]:null;if(d)h(f).on("scroll.DT",function(){var a=this.scrollLeft;k.scrollLeft=a;m&&(u.scrollLeft=a)});h(f).css(e&&c.bCollapse?"max-height":"height",e);
+a.nScrollHead=k;a.nScrollBody=f;a.nScrollFoot=u;a.aoDrawCallback.push({fn:ma,sName:"scrolling"});return i[0]}function ma(a){var b=a.oScroll,c=b.sX,d=b.sXInner,e=b.sY,b=b.iBarWidth,f=h(a.nScrollHead),g=f[0].style,j=f.children("div"),i=j[0].style,l=j.children("table"),j=a.nScrollBody,m=h(j),q=j.style,u=h(a.nScrollFoot).children("div"),n=u.children("table"),o=h(a.nTHead),p=h(a.nTable),r=p[0],t=r.style,s=a.nTFoot?h(a.nTFoot):null,U=a.oBrowser,V=U.bScrollOversize,Yb=C(a.aoColumns,"nTh"),Q,L,R,xa,v=[],
+x=[],y=[],z=[],A,B=function(a){a=a.style;a.paddingTop="0";a.paddingBottom="0";a.borderTopWidth="0";a.borderBottomWidth="0";a.height=0};L=j.scrollHeight>j.clientHeight;if(a.scrollBarVis!==L&&a.scrollBarVis!==k)a.scrollBarVis=L,aa(a);else{a.scrollBarVis=L;p.children("thead, tfoot").remove();s&&(R=s.clone().prependTo(p),Q=s.find("tr"),R=R.find("tr"));xa=o.clone().prependTo(p);o=o.find("tr");L=xa.find("tr");xa.find("th, td").removeAttr("tabindex");c||(q.width="100%",f[0].style.width="100%");h.each(sa(a,
+xa),function(b,c){A=ba(a,b);c.style.width=a.aoColumns[A].sWidth});s&&I(function(a){a.style.width=""},R);f=p.outerWidth();if(""===c){t.width="100%";if(V&&(p.find("tbody").height()>j.offsetHeight||"scroll"==m.css("overflow-y")))t.width=w(p.outerWidth()-b);f=p.outerWidth()}else""!==d&&(t.width=w(d),f=p.outerWidth());I(B,L);I(function(a){y.push(a.innerHTML);v.push(w(h(a).css("width")))},L);I(function(a,b){if(h.inArray(a,Yb)!==-1)a.style.width=v[b]},o);h(L).height(0);s&&(I(B,R),I(function(a){z.push(a.innerHTML);
+x.push(w(h(a).css("width")))},R),I(function(a,b){a.style.width=x[b]},Q),h(R).height(0));I(function(a,b){a.innerHTML=''+y[b]+"
";a.childNodes[0].style.height="0";a.childNodes[0].style.overflow="hidden";a.style.width=v[b]},L);s&&I(function(a,b){a.innerHTML=''+z[b]+"
";a.childNodes[0].style.height="0";a.childNodes[0].style.overflow="hidden";a.style.width=x[b]},R);if(p.outerWidth()j.offsetHeight||"scroll"==m.css("overflow-y")?
+f+b:f;if(V&&(j.scrollHeight>j.offsetHeight||"scroll"==m.css("overflow-y")))t.width=w(Q-b);(""===c||""!==d)&&K(a,1,"Possible column misalignment",6)}else Q="100%";q.width=w(Q);g.width=w(Q);s&&(a.nScrollFoot.style.width=w(Q));!e&&V&&(q.height=w(r.offsetHeight+b));c=p.outerWidth();l[0].style.width=w(c);i.width=w(c);d=p.height()>j.clientHeight||"scroll"==m.css("overflow-y");e="padding"+(U.bScrollbarLeft?"Left":"Right");i[e]=d?b+"px":"0px";s&&(n[0].style.width=w(c),u[0].style.width=w(c),u[0].style[e]=
+d?b+"px":"0px");p.children("colgroup").insertBefore(p.children("thead"));m.trigger("scroll");if((a.bSorted||a.bFiltered)&&!a._drawHold)j.scrollTop=0}}function I(a,b,c){for(var d=0,e=0,f=b.length,g,j;e").appendTo(j.find("tbody"));j.find("thead, tfoot").remove();j.append(h(a.nTHead).clone()).append(h(a.nTFoot).clone());
+j.find("tfoot th, tfoot td").css("width","");l=sa(a,j.find("thead")[0]);for(n=0;n ").css({width:o.sWidthOrig,margin:0,padding:0,border:0,height:1}));if(a.aoData.length)for(n=0;n ").css(f||e?{position:"absolute",top:0,left:0,height:1,
+right:0,overflow:"hidden"}:{}).append(j).appendTo(k);f&&g?j.width(g):f?(j.css("width","auto"),j.removeAttr("width"),j.width()").css("width",w(a)).appendTo(b||H.body),d=c[0].offsetWidth;c.remove();return d}function Hb(a,b){var c=Ib(a,b);if(0>c)return null;var d=a.aoData[c];return!d.nTr?h(" ").html(B(a,c,b,"display"))[0]:d.anCells[b]}function Ib(a,b){for(var c,d=-1,e=-1,f=0,g=a.aoData.length;fd&&(d=c.length,e=f);return e}function w(a){return null===
+a?"0px":"number"==typeof a?0>a?"0px":a+"px":a.match(/\d$/)?a+"px":a}function Y(a){var b,c,d=[],e=a.aoColumns,f,g,j,i;b=a.aaSortingFixed;c=h.isPlainObject(b);var l=[];f=function(a){a.length&&!h.isArray(a[0])?l.push(a):h.merge(l,a)};h.isArray(b)&&f(b);c&&b.pre&&f(b.pre);f(a.aaSorting);c&&b.post&&f(b.post);for(a=0;ae?1:0,0!==c)return"asc"===j.dir?c:-c;c=d[a];e=d[b];
+return ce?1:0}):i.sort(function(a,b){var c,g,j,i,k=h.length,n=f[a]._aSortData,o=f[b]._aSortData;for(j=0;jg?1:0})}a.bSorted=!0}function Kb(a){for(var b,c,d=a.aoColumns,e=Y(a),a=a.oLanguage.oAria,f=0,g=d.length;f/g,"");var i=c.nTh;i.removeAttribute("aria-sort");c.bSortable&&(0e?e+1:3));e=0;for(f=d.length;ee?e+1:3))}a.aLastSort=d}function Jb(a,b){var c=a.aoColumns[b],d=n.ext.order[c.sSortDataType],e;d&&(e=d.call(a.oInstance,a,b,ca(a,b)));for(var f,g=n.ext.type.order[c.sType+"-pre"],j=0,i=a.aoData.length;j=f.length?[0,c[1]]:c)}));b.search!==k&&h.extend(a.oPreviousSearch,Db(b.search));if(b.columns){d=0;for(e=b.columns.length;d=c&&(b=
+c-d);b-=b%d;if(-1===d||0>b)b=0;a._iDisplayStart=b}function Pa(a,b){var c=a.renderer,d=n.ext.renderer[b];return h.isPlainObject(c)&&c[b]?d[c[b]]||d._:"string"===typeof c?d[c]||d._:d._}function y(a){return a.oFeatures.bServerSide?"ssp":a.ajax||a.sAjaxSource?"ajax":"dom"}function ja(a,b){var c=[],c=Mb.numbers_length,d=Math.floor(c/2);b<=c?c=Z(0,b):a<=d?(c=Z(0,c-2),c.push("ellipsis"),c.push(b-1)):(a>=b-1-d?c=Z(b-(c-2),b):(c=Z(a-d+2,a+d-1),c.push("ellipsis"),c.push(b-1)),c.splice(0,0,"ellipsis"),c.splice(0,
+0,0));c.DT_el="span";return c}function Fa(a){h.each({num:function(b){return Ba(b,a)},"num-fmt":function(b){return Ba(b,a,Za)},"html-num":function(b){return Ba(b,a,Ca)},"html-num-fmt":function(b){return Ba(b,a,Ca,Za)}},function(b,c){v.type.order[b+a+"-pre"]=c;b.match(/^html\-/)&&(v.type.search[b+a]=v.type.search.html)})}function Nb(a){return function(){var b=[Aa(this[n.ext.iApiIndex])].concat(Array.prototype.slice.call(arguments));return n.ext.internal[a].apply(this,b)}}var n=function(a){this.$=function(a,
+b){return this.api(!0).$(a,b)};this._=function(a,b){return this.api(!0).rows(a,b).data()};this.api=function(a){return a?new r(Aa(this[v.iApiIndex])):new r(this)};this.fnAddData=function(a,b){var c=this.api(!0),d=h.isArray(a)&&(h.isArray(a[0])||h.isPlainObject(a[0]))?c.rows.add(a):c.row.add(a);(b===k||b)&&c.draw();return d.flatten().toArray()};this.fnAdjustColumnSizing=function(a){var b=this.api(!0).columns.adjust(),c=b.settings()[0],d=c.oScroll;a===k||a?b.draw(!1):(""!==d.sX||""!==d.sY)&&ma(c)};this.fnClearTable=
+function(a){var b=this.api(!0).clear();(a===k||a)&&b.draw()};this.fnClose=function(a){this.api(!0).row(a).child.hide()};this.fnDeleteRow=function(a,b,c){var d=this.api(!0),a=d.rows(a),e=a.settings()[0],h=e.aoData[a[0][0]];a.remove();b&&b.call(this,e,h);(c===k||c)&&d.draw();return h};this.fnDestroy=function(a){this.api(!0).destroy(a)};this.fnDraw=function(a){this.api(!0).draw(a)};this.fnFilter=function(a,b,c,d,e,h){e=this.api(!0);null===b||b===k?e.search(a,c,d,h):e.column(b).search(a,c,d,h);e.draw()};
+this.fnGetData=function(a,b){var c=this.api(!0);if(a!==k){var d=a.nodeName?a.nodeName.toLowerCase():"";return b!==k||"td"==d||"th"==d?c.cell(a,b).data():c.row(a).data()||null}return c.data().toArray()};this.fnGetNodes=function(a){var b=this.api(!0);return a!==k?b.row(a).node():b.rows().nodes().flatten().toArray()};this.fnGetPosition=function(a){var b=this.api(!0),c=a.nodeName.toUpperCase();return"TR"==c?b.row(a).index():"TD"==c||"TH"==c?(a=b.cell(a).index(),[a.row,a.columnVisible,a.column]):null};
+this.fnIsOpen=function(a){return this.api(!0).row(a).child.isShown()};this.fnOpen=function(a,b,c){return this.api(!0).row(a).child(b,c).show().child()[0]};this.fnPageChange=function(a,b){var c=this.api(!0).page(a);(b===k||b)&&c.draw(!1)};this.fnSetColumnVis=function(a,b,c){a=this.api(!0).column(a).visible(b);(c===k||c)&&a.columns.adjust().draw()};this.fnSettings=function(){return Aa(this[v.iApiIndex])};this.fnSort=function(a){this.api(!0).order(a).draw()};this.fnSortListener=function(a,b,c){this.api(!0).order.listener(a,
+b,c)};this.fnUpdate=function(a,b,c,d,e){var h=this.api(!0);c===k||null===c?h.row(b).data(a):h.cell(b,c).data(a);(e===k||e)&&h.columns.adjust();(d===k||d)&&h.draw();return 0};this.fnVersionCheck=v.fnVersionCheck;var b=this,c=a===k,d=this.length;c&&(a={});this.oApi=this.internal=v.internal;for(var e in n.ext.internal)e&&(this[e]=Nb(e));this.each(function(){var e={},g=1 ").appendTo(q));p.nTHead=b[0];b=q.children("tbody");b.length===0&&(b=h(" ").appendTo(q));p.nTBody=b[0];b=q.children("tfoot");if(b.length===0&&a.length>0&&(p.oScroll.sX!==""||p.oScroll.sY!==""))b=h(" ").appendTo(q);if(b.length===0||b.children().length===0)q.addClass(s.sNoFooter);else if(b.length>0){p.nTFoot=b[0];fa(p.aoFooter,p.nTFoot)}if(g.aaData)for(j=0;j/g,$b=/^\d{2,4}[\.\/\-]\d{1,2}[\.\/\-]\d{1,2}([T ]{1}\d{1,2}[:\.]\d{2}([\.:]\d{2})?)?$/,ac=RegExp("(\\/|\\.|\\*|\\+|\\?|\\||\\(|\\)|\\[|\\]|\\{|\\}|\\\\|\\$|\\^|\\-)","g"),Za=/[',$£€¥%\u2009\u202F\u20BD\u20a9\u20BArfkɃΞ]/gi,M=function(a){return!a||
+!0===a||"-"===a?!0:!1},Pb=function(a){var b=parseInt(a,10);return!isNaN(b)&&isFinite(a)?b:null},Qb=function(a,b){$a[b]||($a[b]=RegExp(Sa(b),"g"));return"string"===typeof a&&"."!==b?a.replace(/\./g,"").replace($a[b],"."):a},ab=function(a,b,c){var d="string"===typeof a;if(M(a))return!0;b&&d&&(a=Qb(a,b));c&&d&&(a=a.replace(Za,""));return!isNaN(parseFloat(a))&&isFinite(a)},Rb=function(a,b,c){return M(a)?!0:!(M(a)||"string"===typeof a)?null:ab(a.replace(Ca,""),b,c)?!0:null},C=function(a,b,c){var d=[],
+e=0,f=a.length;if(c!==k)for(;ea.length)){b=a.slice().sort();
+for(var c=b[0],d=1,e=b.length;d")[0],Xb=wa.textContent!==k,Zb=/<.*?>/g,Qa=n.util.throttle,Tb=[],x=Array.prototype,bc=function(a){var b,c,d=n.settings,e=h.map(d,function(a){return a.nTable});if(a){if(a.nTable&&a.oApi)return[a];if(a.nodeName&&"table"===a.nodeName.toLowerCase())return b=h.inArray(a,e),-1!==b?[d[b]]:null;if(a&&"function"===typeof a.settings)return a.settings().toArray();"string"===typeof a?c=h(a):a instanceof h&&(c=a)}else return[];if(c)return c.map(function(){b=
+h.inArray(this,e);return-1!==b?d[b]:null}).toArray()};r=function(a,b){if(!(this instanceof r))return new r(a,b);var c=[],d=function(a){(a=bc(a))&&c.push.apply(c,a)};if(h.isArray(a))for(var e=0,f=a.length;ea?new r(b[a],this[a]):null},filter:function(a){var b=[];if(x.filter)b=x.filter.call(this,a,this);else for(var c=0,d=this.length;c ").addClass(b),h("td",c).addClass(b).html(a)[0].colSpan=W(d),e.push(c[0]))};f(a,b);c._details&&c._details.detach();c._details=h(e);c._detailsShow&&c._details.insertAfter(c.nTr)}return this});o(["row().child.show()","row().child().show()"],function(){Vb(this,!0);return this});o(["row().child.hide()","row().child().hide()"],function(){Vb(this,!1);return this});o(["row().child.remove()","row().child().remove()"],function(){eb(this);return this});o("row().child.isShown()",function(){var a=
+this.context;return a.length&&this.length?a[0].aoData[this[0]]._detailsShow||!1:!1});var cc=/^([^:]+):(name|visIdx|visible)$/,Wb=function(a,b,c,d,e){for(var c=[],d=0,f=e.length;d=0?b:g.length+b];if(typeof a===
+"function"){var e=Da(c,f);return h.map(g,function(b,f){return a(f,Wb(c,f,0,0,e),i[f])?f:null})}var k=typeof a==="string"?a.match(cc):"";if(k)switch(k[2]){case "visIdx":case "visible":b=parseInt(k[1],10);if(b<0){var n=h.map(g,function(a,b){return a.bVisible?b:null});return[n[n.length+b]]}return[ba(c,b)];case "name":return h.map(j,function(a,b){return a===k[1]?b:null});default:return[]}if(a.nodeName&&a._DT_CellIndex)return[a._DT_CellIndex.column];b=h(i).filter(a).map(function(){return h.inArray(this,
+i)}).toArray();if(b.length||!a.nodeName)return b;b=h(a).closest("*[data-dt-column]");return b.length?[b.data("dt-column")]:[]},c,f)},1);c.selector.cols=a;c.selector.opts=b;return c});s("columns().header()","column().header()",function(){return this.iterator("column",function(a,b){return a.aoColumns[b].nTh},1)});s("columns().footer()","column().footer()",function(){return this.iterator("column",function(a,b){return a.aoColumns[b].nTf},1)});s("columns().data()","column().data()",function(){return this.iterator("column-rows",
+Wb,1)});s("columns().dataSrc()","column().dataSrc()",function(){return this.iterator("column",function(a,b){return a.aoColumns[b].mData},1)});s("columns().cache()","column().cache()",function(a){return this.iterator("column-rows",function(b,c,d,e,f){return ka(b.aoData,f,"search"===a?"_aFilterData":"_aSortData",c)},1)});s("columns().nodes()","column().nodes()",function(){return this.iterator("column-rows",function(a,b,c,d,e){return ka(a.aoData,e,"anCells",b)},1)});s("columns().visible()","column().visible()",
+function(a,b){var c=this,d=this.iterator("column",function(b,c){if(a===k)return b.aoColumns[c].bVisible;var d=b.aoColumns,j=d[c],i=b.aoData,l,m,n;if(a!==k&&j.bVisible!==a){if(a){var o=h.inArray(!0,C(d,"bVisible"),c+1);l=0;for(m=i.length;ld;return!0};n.isDataTable=n.fnIsDataTable=function(a){var b=h(a).get(0),c=!1;if(a instanceof n.Api)return!0;h.each(n.settings,function(a,e){var f=e.nScrollHead?h("table",e.nScrollHead)[0]:null,g=e.nScrollFoot?h("table",e.nScrollFoot)[0]:null;if(e.nTable===b||f===b||g===b)c=!0});return c};n.tables=n.fnTables=function(a){var b=
+!1;h.isPlainObject(a)&&(b=a.api,a=a.visible);var c=h.map(n.settings,function(b){if(!a||a&&h(b.nTable).is(":visible"))return b.nTable});return b?new r(c):c};n.camelToHungarian=J;o("$()",function(a,b){var c=this.rows(b).nodes(),c=h(c);return h([].concat(c.filter(a).toArray(),c.find(a).toArray()))});h.each(["on","one","off"],function(a,b){o(b+"()",function(){var a=Array.prototype.slice.call(arguments);a[0]=h.map(a[0].split(/\s/),function(a){return!a.match(/\.dt\b/)?a+".dt":a}).join(" ");var d=h(this.tables().nodes());
+d[b].apply(d,a);return this})});o("clear()",function(){return this.iterator("table",function(a){pa(a)})});o("settings()",function(){return new r(this.context,this.context)});o("init()",function(){var a=this.context;return a.length?a[0].oInit:null});o("data()",function(){return this.iterator("table",function(a){return C(a.aoData,"_aData")}).flatten()});o("destroy()",function(a){a=a||!1;return this.iterator("table",function(b){var c=b.nTableWrapper.parentNode,d=b.oClasses,e=b.nTable,f=b.nTBody,g=b.nTHead,
+j=b.nTFoot,i=h(e),f=h(f),k=h(b.nTableWrapper),m=h.map(b.aoData,function(a){return a.nTr}),o;b.bDestroying=!0;t(b,"aoDestroyCallback","destroy",[b]);a||(new r(b)).columns().visible(!0);k.off(".DT").find(":not(tbody *)").off(".DT");h(E).off(".DT-"+b.sInstance);e!=g.parentNode&&(i.children("thead").detach(),i.append(g));j&&e!=j.parentNode&&(i.children("tfoot").detach(),i.append(j));b.aaSorting=[];b.aaSortingFixed=[];ya(b);h(m).removeClass(b.asStripeClasses.join(" "));h("th, td",g).removeClass(d.sSortable+
+" "+d.sSortableAsc+" "+d.sSortableDesc+" "+d.sSortableNone);f.children().detach();f.append(m);g=a?"remove":"detach";i[g]();k[g]();!a&&c&&(c.insertBefore(e,b.nTableReinsertBefore),i.css("width",b.sDestroyWidth).removeClass(d.sTable),(o=b.asDestroyStripes.length)&&f.children().each(function(a){h(this).addClass(b.asDestroyStripes[a%o])}));c=h.inArray(b,n.settings);-1!==c&&n.settings.splice(c,1)})});h.each(["column","row","cell"],function(a,b){o(b+"s().every()",function(a){var d=this.selector.opts,e=
+this;return this.iterator(b,function(f,g,h,i,l){a.call(e[b](g,"cell"===b?h:d,"cell"===b?d:k),g,h,i,l)})})});o("i18n()",function(a,b,c){var d=this.context[0],a=S(a)(d.oLanguage);a===k&&(a=b);c!==k&&h.isPlainObject(a)&&(a=a[c]!==k?a[c]:a._);return a.replace("%d",c)});n.version="1.10.20";n.settings=[];n.models={};n.models.oSearch={bCaseInsensitive:!0,sSearch:"",bRegex:!1,bSmart:!0};n.models.oRow={nTr:null,anCells:null,_aData:[],_aSortData:null,_aFilterData:null,_sFilterRow:null,_sRowStripe:"",src:null,
+idx:-1};n.models.oColumn={idx:null,aDataSort:null,asSorting:null,bSearchable:null,bSortable:null,bVisible:null,_sManualType:null,_bAttrSrc:!1,fnCreatedCell:null,fnGetData:null,fnSetData:null,mData:null,mRender:null,nTh:null,nTf:null,sClass:null,sContentPadding:null,sDefaultContent:null,sName:null,sSortDataType:"std",sSortingClass:null,sSortingClassJUI:null,sTitle:null,sType:null,sWidth:null,sWidthOrig:null};n.defaults={aaData:null,aaSorting:[[0,"asc"]],aaSortingFixed:[],ajax:null,aLengthMenu:[10,
+25,50,100],aoColumns:null,aoColumnDefs:null,aoSearchCols:[],asStripeClasses:null,bAutoWidth:!0,bDeferRender:!1,bDestroy:!1,bFilter:!0,bInfo:!0,bLengthChange:!0,bPaginate:!0,bProcessing:!1,bRetrieve:!1,bScrollCollapse:!1,bServerSide:!1,bSort:!0,bSortMulti:!0,bSortCellsTop:!1,bSortClasses:!0,bStateSave:!1,fnCreatedRow:null,fnDrawCallback:null,fnFooterCallback:null,fnFormatNumber:function(a){return a.toString().replace(/\B(?=(\d{3})+(?!\d))/g,this.oLanguage.sThousands)},fnHeaderCallback:null,fnInfoCallback:null,
+fnInitComplete:null,fnPreDrawCallback:null,fnRowCallback:null,fnServerData:null,fnServerParams:null,fnStateLoadCallback:function(a){try{return JSON.parse((-1===a.iStateDuration?sessionStorage:localStorage).getItem("DataTables_"+a.sInstance+"_"+location.pathname))}catch(b){}},fnStateLoadParams:null,fnStateLoaded:null,fnStateSaveCallback:function(a,b){try{(-1===a.iStateDuration?sessionStorage:localStorage).setItem("DataTables_"+a.sInstance+"_"+location.pathname,JSON.stringify(b))}catch(c){}},fnStateSaveParams:null,
+iStateDuration:7200,iDeferLoading:null,iDisplayLength:10,iDisplayStart:0,iTabIndex:0,oClasses:{},oLanguage:{oAria:{sSortAscending:": activate to sort column ascending",sSortDescending:": activate to sort column descending"},oPaginate:{sFirst:"First",sLast:"Last",sNext:"Next",sPrevious:"Previous"},sEmptyTable:"No data available in table",sInfo:"Showing _START_ to _END_ of _TOTAL_ entries",sInfoEmpty:"Showing 0 to 0 of 0 entries",sInfoFiltered:"(filtered from _MAX_ total entries)",sInfoPostFix:"",sDecimal:"",
+sThousands:",",sLengthMenu:"Show _MENU_ entries",sLoadingRecords:"Loading...",sProcessing:"Processing...",sSearch:"Search:",sSearchPlaceholder:"",sUrl:"",sZeroRecords:"No matching records found"},oSearch:h.extend({},n.models.oSearch),sAjaxDataProp:"data",sAjaxSource:null,sDom:"lfrtip",searchDelay:null,sPaginationType:"simple_numbers",sScrollX:"",sScrollXInner:"",sScrollY:"",sServerMethod:"GET",renderer:null,rowId:"DT_RowId"};$(n.defaults);n.defaults.column={aDataSort:null,iDataSort:-1,asSorting:["asc",
+"desc"],bSearchable:!0,bSortable:!0,bVisible:!0,fnCreatedCell:null,mData:null,mRender:null,sCellType:"td",sClass:"",sContentPadding:"",sDefaultContent:null,sName:"",sSortDataType:"std",sTitle:null,sType:null,sWidth:null};$(n.defaults.column);n.models.oSettings={oFeatures:{bAutoWidth:null,bDeferRender:null,bFilter:null,bInfo:null,bLengthChange:null,bPaginate:null,bProcessing:null,bServerSide:null,bSort:null,bSortMulti:null,bSortClasses:null,bStateSave:null},oScroll:{bCollapse:null,iBarWidth:0,sX:null,
+sXInner:null,sY:null},oLanguage:{fnInfoCallback:null},oBrowser:{bScrollOversize:!1,bScrollbarLeft:!1,bBounding:!1,barWidth:0},ajax:null,aanFeatures:[],aoData:[],aiDisplay:[],aiDisplayMaster:[],aIds:{},aoColumns:[],aoHeader:[],aoFooter:[],oPreviousSearch:{},aoPreSearchCols:[],aaSorting:null,aaSortingFixed:[],asStripeClasses:null,asDestroyStripes:[],sDestroyWidth:0,aoRowCallback:[],aoHeaderCallback:[],aoFooterCallback:[],aoDrawCallback:[],aoRowCreatedCallback:[],aoPreDrawCallback:[],aoInitComplete:[],
+aoStateSaveParams:[],aoStateLoadParams:[],aoStateLoaded:[],sTableId:"",nTable:null,nTHead:null,nTFoot:null,nTBody:null,nTableWrapper:null,bDeferLoading:!1,bInitialised:!1,aoOpenRows:[],sDom:null,searchDelay:null,sPaginationType:"two_button",iStateDuration:0,aoStateSave:[],aoStateLoad:[],oSavedState:null,oLoadedState:null,sAjaxSource:null,sAjaxDataProp:null,bAjaxDataGet:!0,jqXHR:null,json:k,oAjaxData:k,fnServerData:null,aoServerParams:[],sServerMethod:null,fnFormatNumber:null,aLengthMenu:null,iDraw:0,
+bDrawing:!1,iDrawError:-1,_iDisplayLength:10,_iDisplayStart:0,_iRecordsTotal:0,_iRecordsDisplay:0,oClasses:{},bFiltered:!1,bSorted:!1,bSortCellsTop:null,oInit:null,aoDestroyCallback:[],fnRecordsTotal:function(){return"ssp"==y(this)?1*this._iRecordsTotal:this.aiDisplayMaster.length},fnRecordsDisplay:function(){return"ssp"==y(this)?1*this._iRecordsDisplay:this.aiDisplay.length},fnDisplayEnd:function(){var a=this._iDisplayLength,b=this._iDisplayStart,c=b+a,d=this.aiDisplay.length,e=this.oFeatures,f=
+e.bPaginate;return e.bServerSide?!1===f||-1===a?b+d:Math.min(b+a,this._iRecordsDisplay):!f||c>d||-1===a?d:c},oInstance:null,sInstance:null,iTabIndex:0,nScrollHead:null,nScrollFoot:null,aLastSort:[],oPlugins:{},rowIdFn:null,rowId:null};n.ext=v={buttons:{},classes:{},builder:"-source-",errMode:"alert",feature:[],search:[],selector:{cell:[],column:[],row:[]},internal:{},legacy:{ajax:null},pager:{},renderer:{pageButton:{},header:{}},order:{},type:{detect:[],search:{},order:{}},_unique:0,fnVersionCheck:n.fnVersionCheck,
+iApiIndex:0,oJUIClasses:{},sVersion:n.version};h.extend(v,{afnFiltering:v.search,aTypes:v.type.detect,ofnSearch:v.type.search,oSort:v.type.order,afnSortData:v.order,aoFeatures:v.feature,oApi:v.internal,oStdClasses:v.classes,oPagination:v.pager});h.extend(n.ext.classes,{sTable:"dataTable",sNoFooter:"no-footer",sPageButton:"paginate_button",sPageButtonActive:"current",sPageButtonDisabled:"disabled",sStripeOdd:"odd",sStripeEven:"even",sRowEmpty:"dataTables_empty",sWrapper:"dataTables_wrapper",sFilter:"dataTables_filter",
+sInfo:"dataTables_info",sPaging:"dataTables_paginate paging_",sLength:"dataTables_length",sProcessing:"dataTables_processing",sSortAsc:"sorting_asc",sSortDesc:"sorting_desc",sSortable:"sorting",sSortableAsc:"sorting_asc_disabled",sSortableDesc:"sorting_desc_disabled",sSortableNone:"sorting_disabled",sSortColumn:"sorting_",sFilterInput:"",sLengthSelect:"",sScrollWrapper:"dataTables_scroll",sScrollHead:"dataTables_scrollHead",sScrollHeadInner:"dataTables_scrollHeadInner",sScrollBody:"dataTables_scrollBody",
+sScrollFoot:"dataTables_scrollFoot",sScrollFootInner:"dataTables_scrollFootInner",sHeaderTH:"",sFooterTH:"",sSortJUIAsc:"",sSortJUIDesc:"",sSortJUI:"",sSortJUIAscAllowed:"",sSortJUIDescAllowed:"",sSortJUIWrapper:"",sSortIcon:"",sJUIHeader:"",sJUIFooter:""});var Mb=n.ext.pager;h.extend(Mb,{simple:function(){return["previous","next"]},full:function(){return["first","previous","next","last"]},numbers:function(a,b){return[ja(a,b)]},simple_numbers:function(a,b){return["previous",ja(a,b),"next"]},full_numbers:function(a,
+b){return["first","previous",ja(a,b),"next","last"]},first_last_numbers:function(a,b){return["first",ja(a,b),"last"]},_numbers:ja,numbers_length:7});h.extend(!0,n.ext.renderer,{pageButton:{_:function(a,b,c,d,e,f){var g=a.oClasses,j=a.oLanguage.oPaginate,i=a.oLanguage.oAria.paginate||{},l,m,n=0,o=function(b,d){var k,s,r,t,v=g.sPageButtonDisabled,w=function(b){Va(a,b.data.action,true)};k=0;for(s=d.length;k ").appendTo(b);o(r,t)}else{l=null;
+m=t;r=a.iTabIndex;switch(t){case "ellipsis":b.append('… ');break;case "first":l=j.sFirst;if(e===0){r=-1;m=m+(" "+v)}break;case "previous":l=j.sPrevious;if(e===0){r=-1;m=m+(" "+v)}break;case "next":l=j.sNext;if(e===f-1){r=-1;m=m+(" "+v)}break;case "last":l=j.sLast;if(e===f-1){r=-1;m=m+(" "+v)}break;default:l=t+1;m=e===t?g.sPageButtonActive:""}if(l!==null){r=h("",{"class":g.sPageButton+" "+m,"aria-controls":a.sTableId,"aria-label":i[t],"data-dt-idx":n,tabindex:r,
+id:c===0&&typeof t==="string"?a.sTableId+"_"+t:null}).html(l).appendTo(b);Xa(r,{action:t},w);n++}}}},s;try{s=h(b).find(H.activeElement).data("dt-idx")}catch(r){}o(h(b).empty(),d);s!==k&&h(b).find("[data-dt-idx="+s+"]").focus()}}});h.extend(n.ext.type.detect,[function(a,b){var c=b.oLanguage.sDecimal;return ab(a,c)?"num"+c:null},function(a){if(a&&!(a instanceof Date)&&!$b.test(a))return null;var b=Date.parse(a);return null!==b&&!isNaN(b)||M(a)?"date":null},function(a,b){var c=b.oLanguage.sDecimal;return ab(a,
+c,!0)?"num-fmt"+c:null},function(a,b){var c=b.oLanguage.sDecimal;return Rb(a,c)?"html-num"+c:null},function(a,b){var c=b.oLanguage.sDecimal;return Rb(a,c,!0)?"html-num-fmt"+c:null},function(a){return M(a)||"string"===typeof a&&-1!==a.indexOf("<")?"html":null}]);h.extend(n.ext.type.search,{html:function(a){return M(a)?a:"string"===typeof a?a.replace(Ob," ").replace(Ca,""):""},string:function(a){return M(a)?a:"string"===typeof a?a.replace(Ob," "):a}});var Ba=function(a,b,c,d){if(0!==a&&(!a||"-"===a))return-Infinity;
+b&&(a=Qb(a,b));a.replace&&(c&&(a=a.replace(c,"")),d&&(a=a.replace(d,"")));return 1*a};h.extend(v.type.order,{"date-pre":function(a){a=Date.parse(a);return isNaN(a)?-Infinity:a},"html-pre":function(a){return M(a)?"":a.replace?a.replace(/<.*?>/g,"").toLowerCase():a+""},"string-pre":function(a){return M(a)?"":"string"===typeof a?a.toLowerCase():!a.toString?"":a.toString()},"string-asc":function(a,b){return ab?1:0},"string-desc":function(a,b){return ab?-1:0}});Fa("");h.extend(!0,n.ext.renderer,
+{header:{_:function(a,b,c,d){h(a.nTable).on("order.dt.DT",function(e,f,g,h){if(a===f){e=c.idx;b.removeClass(c.sSortingClass+" "+d.sSortAsc+" "+d.sSortDesc).addClass(h[e]=="asc"?d.sSortAsc:h[e]=="desc"?d.sSortDesc:c.sSortingClass)}})},jqueryui:function(a,b,c,d){h("
").addClass(d.sSortJUIWrapper).append(b.contents()).append(h(" ").addClass(d.sSortIcon+" "+c.sSortingClassJUI)).appendTo(b);h(a.nTable).on("order.dt.DT",function(e,f,g,h){if(a===f){e=c.idx;b.removeClass(d.sSortAsc+" "+d.sSortDesc).addClass(h[e]==
+"asc"?d.sSortAsc:h[e]=="desc"?d.sSortDesc:c.sSortingClass);b.find("span."+d.sSortIcon).removeClass(d.sSortJUIAsc+" "+d.sSortJUIDesc+" "+d.sSortJUI+" "+d.sSortJUIAscAllowed+" "+d.sSortJUIDescAllowed).addClass(h[e]=="asc"?d.sSortJUIAsc:h[e]=="desc"?d.sSortJUIDesc:c.sSortingClassJUI)}})}}});var fb=function(a){return"string"===typeof a?a.replace(/ /g,">").replace(/"/g,"""):a};n.render={number:function(a,b,c,d,e){return{display:function(f){if("number"!==typeof f&&"string"!==
+typeof f)return f;var g=0>f?"-":"",h=parseFloat(f);if(isNaN(h))return fb(f);h=h.toFixed(c);f=Math.abs(h);h=parseInt(f,10);f=c?b+(f-h).toFixed(c).substring(2):"";return g+(d||"")+h.toString().replace(/\B(?=(\d{3})+(?!\d))/g,a)+f+(e||"")}}},text:function(){return{display:fb,filter:fb}}};h.extend(n.ext.internal,{_fnExternApiFunc:Nb,_fnBuildAjax:ta,_fnAjaxUpdate:nb,_fnAjaxParameters:wb,_fnAjaxUpdateDraw:xb,_fnAjaxDataSrc:ua,_fnAddColumn:Ga,_fnColumnOptions:la,_fnAdjustColumnSizing:aa,_fnVisibleToColumnIndex:ba,
+_fnColumnIndexToVisible:ca,_fnVisbleColumns:W,_fnGetColumns:na,_fnColumnTypes:Ia,_fnApplyColumnDefs:kb,_fnHungarianMap:$,_fnCamelToHungarian:J,_fnLanguageCompat:Ea,_fnBrowserDetect:ib,_fnAddData:O,_fnAddTr:oa,_fnNodeToDataIndex:function(a,b){return b._DT_RowIndex!==k?b._DT_RowIndex:null},_fnNodeToColumnIndex:function(a,b,c){return h.inArray(c,a.aoData[b].anCells)},_fnGetCellData:B,_fnSetCellData:lb,_fnSplitObjNotation:La,_fnGetObjectDataFn:S,_fnSetObjectDataFn:N,_fnGetDataMaster:Ma,_fnClearTable:pa,
+_fnDeleteIndex:qa,_fnInvalidate:ea,_fnGetRowElements:Ka,_fnCreateTr:Ja,_fnBuildHead:mb,_fnDrawHead:ga,_fnDraw:P,_fnReDraw:T,_fnAddOptionsHtml:pb,_fnDetectHeader:fa,_fnGetUniqueThs:sa,_fnFeatureHtmlFilter:rb,_fnFilterComplete:ha,_fnFilterCustom:Ab,_fnFilterColumn:zb,_fnFilter:yb,_fnFilterCreateSearch:Ra,_fnEscapeRegex:Sa,_fnFilterData:Bb,_fnFeatureHtmlInfo:ub,_fnUpdateInfo:Eb,_fnInfoMacros:Fb,_fnInitialise:ia,_fnInitComplete:va,_fnLengthChange:Ta,_fnFeatureHtmlLength:qb,_fnFeatureHtmlPaginate:vb,_fnPageChange:Va,
+_fnFeatureHtmlProcessing:sb,_fnProcessingDisplay:D,_fnFeatureHtmlTable:tb,_fnScrollDraw:ma,_fnApplyToChildren:I,_fnCalculateColumnWidths:Ha,_fnThrottle:Qa,_fnConvertToWidth:Gb,_fnGetWidestNode:Hb,_fnGetMaxLenString:Ib,_fnStringToCss:w,_fnSortFlatten:Y,_fnSort:ob,_fnSortAria:Kb,_fnSortListener:Wa,_fnSortAttachListener:Oa,_fnSortingClasses:ya,_fnSortData:Jb,_fnSaveState:za,_fnLoadState:Lb,_fnSettingsFromNode:Aa,_fnLog:K,_fnMap:F,_fnBindAction:Xa,_fnCallbackReg:z,_fnCallbackFire:t,_fnLengthOverflow:Ua,
+_fnRenderer:Pa,_fnDataSource:y,_fnRowAttributes:Na,_fnExtend:Ya,_fnCalculateEnd:function(){}});h.fn.dataTable=n;n.$=h;h.fn.dataTableSettings=n.settings;h.fn.dataTableExt=n.ext;h.fn.DataTable=function(a){return h(this).dataTable(a).api()};h.each(n,function(a,b){h.fn.DataTable[a]=b});return h.fn.dataTable});
diff --git a/modules/import_api/views/js/front.js b/modules/import_api/views/js/front.js
new file mode 100644
index 00000000..77935cba
--- /dev/null
+++ b/modules/import_api/views/js/front.js
@@ -0,0 +1,27 @@
+/**
+* 2007-2018 PrestaShop
+*
+* NOTICE OF LICENSE
+*
+* This source file is subject to the Academic Free License (AFL 3.0)
+* that is bundled with this package in the file LICENSE.txt.
+* It is also available through the world-wide-web at this URL:
+* http://opensource.org/licenses/afl-3.0.php
+* If you did not receive a copy of the license and are unable to
+* obtain it through the world-wide-web, please send an email
+* to license@prestashop.com so we can send you a copy immediately.
+*
+* DISCLAIMER
+*
+* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
+* versions in the future. If you wish to customize PrestaShop for your
+* needs please refer to http://www.prestashop.com for more information.
+*
+* @author PrestaShop SA
+* @copyright 2007-2018 PrestaShop SA
+* @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0)
+* International Registered Trademark & Property of PrestaShop SA
+*
+* Don't forget to prefix your containers with your own identifier
+* to avoid any conflicts with others containers.
+*/
diff --git a/modules/import_api/views/js/index.php b/modules/import_api/views/js/index.php
new file mode 100644
index 00000000..907720c1
--- /dev/null
+++ b/modules/import_api/views/js/index.php
@@ -0,0 +1,35 @@
+
+* @copyright 2007-2018 PrestaShop SA
+* @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0)
+* International Registered Trademark & Property of PrestaShop SA
+*/
+
+header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
+header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT');
+
+header('Cache-Control: no-store, no-cache, must-revalidate');
+header('Cache-Control: post-check=0, pre-check=0', false);
+header('Pragma: no-cache');
+
+header('Location: ../');
+exit;
diff --git a/modules/import_api/views/templates/admin/configure.tpl b/modules/import_api/views/templates/admin/configure.tpl
new file mode 100644
index 00000000..5f855230
--- /dev/null
+++ b/modules/import_api/views/templates/admin/configure.tpl
@@ -0,0 +1,87 @@
+{*
+* 2007-2018 PrestaShop
+*
+* NOTICE OF LICENSE
+*
+* This source file is subject to the Academic Free License (AFL 3.0)
+* that is bundled with this package in the file LICENSE.txt.
+* It is also available through the world-wide-web at this URL:
+* http://opensource.org/licenses/afl-3.0.php
+* If you did not receive a copy of the license and are unable to
+* obtain it through the world-wide-web, please send an email
+* to license@prestashop.com so we can send you a copy immediately.
+*
+* DISCLAIMER
+*
+* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
+* versions in the future. If you wish to customize PrestaShop for your
+* needs please refer to http://www.prestashop.com for more information.
+*
+* @author PrestaShop SA
+* @copyright 2007-2018 PrestaShop SA
+* @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0)
+* International Registered Trademark & Property of PrestaShop SA
+*}
+
+
+
+
+
Source: {$file_name}
+
+
Please wait while module read file
+
+
+
+
+
+
Reload file
+
Go to import page
+
+
\ No newline at end of file
diff --git a/modules/import_api/views/templates/admin/delete.tpl b/modules/import_api/views/templates/admin/delete.tpl
new file mode 100644
index 00000000..3d5fc07b
--- /dev/null
+++ b/modules/import_api/views/templates/admin/delete.tpl
@@ -0,0 +1,79 @@
+{**
+ * NOTICE OF LICENSE
+ * With the purchase or the installation of the software in your application
+ * you accept the license agreement.
+ *
+ * You can not resell and redistribute this file.
+ *
+ * @author Dalibor Stojcevski
+ * @copyright 2019 Dalibor Stojcevski
+ * @license Dalibor Stojcevski
+ *}
+
+
There are {$total} products from this file {$file_name}. Products will be deleted from prestashop. Press delete button only if you are sure that you don't need products from this file
+
+
+
+
+
diff --git a/modules/import_api/views/templates/admin/file_upload.tpl b/modules/import_api/views/templates/admin/file_upload.tpl
new file mode 100644
index 00000000..eb142b44
--- /dev/null
+++ b/modules/import_api/views/templates/admin/file_upload.tpl
@@ -0,0 +1,118 @@
+{**
+ * NOTICE OF LICENSE
+ * With the purchase or the installation of the software in your application
+ * you accept the license agreement.
+ *
+ * You can not resell and redistribute this file.
+ *
+ * @author Dalibor Stojcevski
+ * @copyright 2019 Dalibor Stojcevski
+ * @license Dalibor Stojcevski
+ *}
+
+
+
\ No newline at end of file
diff --git a/modules/import_api/views/templates/admin/import.tpl b/modules/import_api/views/templates/admin/import.tpl
new file mode 100644
index 00000000..8556c693
--- /dev/null
+++ b/modules/import_api/views/templates/admin/import.tpl
@@ -0,0 +1,199 @@
+{**
+ * NOTICE OF LICENSE
+ * With the purchase or the installation of the software in your application
+ * you accept the license agreement.
+ *
+ * You can not resell and redistribute this file.
+ *
+ * @author Dalibor Stojcevski
+ * @copyright 2019 Dalibor Stojcevski
+ * @license Dalibor Stojcevski
+ *}
+
+
+
+
+
+
+
+
+
+
Imported : NA
+
Updated : NA
+
Total : NA
+
+
For cron job you should use link {$get_import_link|escape:'htmlall':'UTF-8'}
+
+
+
+
+
+
If you have imported products by mistake and they can't be updated. Or you just need to delete products from this file, go to Delete products page
+
+
+
+
diff --git a/modules/import_api/views/templates/admin/index.php b/modules/import_api/views/templates/admin/index.php
new file mode 100644
index 00000000..907720c1
--- /dev/null
+++ b/modules/import_api/views/templates/admin/index.php
@@ -0,0 +1,35 @@
+
+* @copyright 2007-2018 PrestaShop SA
+* @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0)
+* International Registered Trademark & Property of PrestaShop SA
+*/
+
+header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
+header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT');
+
+header('Cache-Control: no-store, no-cache, must-revalidate');
+header('Cache-Control: post-check=0, pre-check=0', false);
+header('Pragma: no-cache');
+
+header('Location: ../');
+exit;
diff --git a/modules/import_api/views/templates/admin/list.tpl b/modules/import_api/views/templates/admin/list.tpl
new file mode 100644
index 00000000..9906224f
--- /dev/null
+++ b/modules/import_api/views/templates/admin/list.tpl
@@ -0,0 +1,35 @@
+{**
+ * NOTICE OF LICENSE
+ * With the purchase or the installation of the software in your application
+ * you accept the license agreement.
+ *
+ * You can not resell and redistribute this file.
+ *
+ * @author Dalibor Stojcevski
+ * @copyright 2019 Dalibor Stojcevski
+ * @license Dalibor Stojcevski
+ *}
+
\ No newline at end of file
diff --git a/modules/import_api/views/templates/admin/temp.tpl b/modules/import_api/views/templates/admin/temp.tpl
new file mode 100644
index 00000000..b376c83a
--- /dev/null
+++ b/modules/import_api/views/templates/admin/temp.tpl
@@ -0,0 +1,193 @@
+{**
+ * NOTICE OF LICENSE
+ * With the purchase or the installation of the software in your application
+ * you accept the license agreement.
+ *
+ * You can not resell and redistribute this file.
+ *
+ * @author Dalibor Stojcevski
+ * @copyright 2019 Dalibor Stojcevski
+ * @license Dalibor Stojcevski
+ *}
+
+ {if !$products}
+
You don't have queued products. It is good to make this process for better testing and view before import. But you can skip and press Start import
+
Queue
+
+ {/if}
+
Please wait while module queue products
+
+
+
+
+ {if $products}
+
+
Products from file
+
Change settings
+
{$file_name}
+
Imported: , Updated:
+
Import selected
+
+
+
+ {if $limit}
+
There is limit ({$limit}) for queued products for better performance. If you want to show all press on link Show all
+ {/if}
+ {/if}
+
+
\ No newline at end of file
diff --git a/modules/import_api/views/templates/index.php b/modules/import_api/views/templates/index.php
new file mode 100644
index 00000000..907720c1
--- /dev/null
+++ b/modules/import_api/views/templates/index.php
@@ -0,0 +1,35 @@
+
+* @copyright 2007-2018 PrestaShop SA
+* @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0)
+* International Registered Trademark & Property of PrestaShop SA
+*/
+
+header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
+header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT');
+
+header('Cache-Control: no-store, no-cache, must-revalidate');
+header('Cache-Control: post-check=0, pre-check=0', false);
+header('Pragma: no-cache');
+
+header('Location: ../');
+exit;