From 2ed8877968e3049995981971cf75b9700c2ece1d Mon Sep 17 00:00:00 2001 From: Gravity Forms Date: Fri, 19 Apr 2024 13:57:37 +0000 Subject: [PATCH] Updates to 1.7 --- assets/css/dist/admin.css | 138 +++ assets/css/dist/admin.min.css | 1 + assets/css/dist/theme-framework.css | 32 + assets/css/dist/theme-framework.min.css | 1 + assets/css/dist/theme.css | 60 ++ assets/css/dist/theme.min.css | 1 + assets/sample.csv | 1109 +++++++++++++++++++++ chainedselects.php | 55 + change_log.txt | 73 ++ class-gf-chainedselects.php | 316 ++++++ images/menu-icon.svg | 1 + includes/class-gf-field-chainedselect.php | 927 +++++++++++++++++ js/admin-form-editor.js | 419 ++++++++ js/admin-form-editor.min.js | 8 + js/admin.js | 110 ++ js/admin.min.js | 1 + js/frontend.js | 318 ++++++ js/frontend.min.js | 1 + languages/gravityformschainedselects.pot | 221 ++++ 19 files changed, 3792 insertions(+) create mode 100644 assets/css/dist/admin.css create mode 100644 assets/css/dist/admin.min.css create mode 100644 assets/css/dist/theme-framework.css create mode 100644 assets/css/dist/theme-framework.min.css create mode 100644 assets/css/dist/theme.css create mode 100644 assets/css/dist/theme.min.css create mode 100644 assets/sample.csv create mode 100644 chainedselects.php create mode 100644 change_log.txt create mode 100644 class-gf-chainedselects.php create mode 100644 images/menu-icon.svg create mode 100644 includes/class-gf-field-chainedselect.php create mode 100644 js/admin-form-editor.js create mode 100644 js/admin-form-editor.min.js create mode 100644 js/admin.js create mode 100644 js/admin.min.js create mode 100644 js/frontend.js create mode 100644 js/frontend.min.js create mode 100644 languages/gravityformschainedselects.pot diff --git a/assets/css/dist/admin.css b/assets/css/dist/admin.css new file mode 100644 index 0000000..5707683 --- /dev/null +++ b/assets/css/dist/admin.css @@ -0,0 +1,138 @@ +/* +---------------------------------------------------------------- + +Gravity Forms Chained Selects Administration Styles +http: //www.gravityforms.com + +Gravity Forms is a Rocketgenius project +copyright 2008-2022 Rocketgenius Inc. +http: //www.rocketgenius.com +this may not be re-distributed without the +express written permission of the author. + +NOTE: DO NOT EDIT THIS FILE! +THIS FILE IS REPLACED DURING AUTO UPGRADE +AND ANY CHANGES MADE HERE WILL BE OVERWRITTEN. + +---------------------------------------------------------------- +*/ + +#gfcs-container { + margin: 0 0 12px; +} + +#gfcs-drop { + border: 2px dashed #ddd; + line-height: 29px; + margin: 0 0 12px; + padding: 30px 0; + text-align: center; + width: 375px; +} + +#gfcs-progress:hover .gfcs-status-complete .gfcs-remove { + opacity: 1; +} + +.gfcs-status-complete .gfcs-remove { + display: inline; + } + +.gfcs-status-complete .gfcs-success { + display: inline; + opacity: 1; + } + +.gfcs-status-complete .gfcs-file-percent { + display: none; + } + +.gfcs-status-processing .gfcs-processing { + display: inline-block; +} + +.gfcs-remove { + color: #800; + cursor: pointer; + display: none; + margin-left: 2px; + opacity: 0; + transition: all 0.25s; +} + +.gfcs-success { + display: none; + margin-left: 2px; + opacity: 0; + transition: all 0.25s; +} + +.gfcs-processing { + display: none; + height: 16px; + position: relative; + top: 3px; + width: 16px; +} + +.gfcs-file-size, .gfcs-file-date { + opacity: 0.5; +} + +.gfcs-file-icon { + display: inline-block; + height: 16px; + position: relative; + top: 3px; + width: 16px; +} + +.gf-dragging { + border: 2px solid #ddd !important; +} + +.gf-dragging * { + opacity: 0.5; +} + +.gfcs-source-message { + margin: 12px 0 !important; +} + +.ginput_chained_selects_container span { + display: inline-block; + padding: 0 4px 0 0; + } + +.ginput_chained_selects_container.vertical span { + display: block; + padding: 0 0 4px; + } + +.ginput_chained_selects_container.vertical select { + max-width: 100%; + min-width: 100px; + } + +@media only screen and (min-width: 782px) { + + .gfcs-value { + display: table; + } + + .gfcs-value-row { + display: table-row; + } + + .gfcs-value b { + display: table-cell; + margin-right: 10px; + } + + .gfcs-value span { + display: table-cell; + } + +} + +/*# sourceMappingURL=admin.css.map */ diff --git a/assets/css/dist/admin.min.css b/assets/css/dist/admin.min.css new file mode 100644 index 0000000..8135e95 --- /dev/null +++ b/assets/css/dist/admin.min.css @@ -0,0 +1 @@ +#gfcs-container,#gfcs-drop{margin:0 0 12px}#gfcs-drop{border:2px dashed #ddd;line-height:29px;padding:30px 0;text-align:center;width:375px}#gfcs-progress:hover .gfcs-status-complete .gfcs-remove{opacity:1}.gfcs-status-complete .gfcs-remove{display:inline}.gfcs-status-complete .gfcs-success{display:inline;opacity:1}.gfcs-status-complete .gfcs-file-percent{display:none}.gfcs-status-processing .gfcs-processing{display:inline-block}.gfcs-remove{color:#800;cursor:pointer}.gfcs-remove,.gfcs-success{display:none;margin-left:2px;opacity:0;transition:all .25s}.gfcs-processing{display:none;height:16px;position:relative;top:3px;width:16px}.gfcs-file-date,.gfcs-file-size{opacity:.5}.gfcs-file-icon{display:inline-block;height:16px;position:relative;top:3px;width:16px}.gf-dragging{border:2px solid #ddd!important}.gf-dragging *{opacity:.5}.gfcs-source-message{margin:12px 0!important}.ginput_chained_selects_container span{display:inline-block;padding:0 4px 0 0}.ginput_chained_selects_container.vertical span{display:block;padding:0 0 4px}.ginput_chained_selects_container.vertical select{max-width:100%;min-width:100px}@media only screen and (min-width:782px){.gfcs-value{display:table}.gfcs-value-row{display:table-row}.gfcs-value b{margin-right:10px}.gfcs-value b,.gfcs-value span{display:table-cell}} \ No newline at end of file diff --git a/assets/css/dist/theme-framework.css b/assets/css/dist/theme-framework.css new file mode 100644 index 0000000..8c4fc36 --- /dev/null +++ b/assets/css/dist/theme-framework.css @@ -0,0 +1,32 @@ +/* +---------------------------------------------------------------- + +theme.css +Gravity Forms Theme Framework & CSS API +For the Chained Selects Add-On +https://www.gravityforms.com + +Theme dependencies: +- Gravity Forms Theme Reset: gravity-forms-theme-reset.css +- Gravity Forms Theme Foundation: gravity-forms-theme-foundation.css + +Gravity Forms is a Rocketgenius project +copyright 2008-2023 Rocketgenius Inc. +https://www.rocketgenius.com +this may not be re-distributed without the +express written permission of the author. + +NOTE: DO NOT EDIT THIS FILE! +THIS FILE IS REPLACED DURING AUTO UPGRADE +AND ANY CHANGES MADE HERE WILL BE OVERWRITTEN. + +---------------------------------------------------------------- +*/ + +.gform-theme--framework .gfield_chainedselect.vertical.ginput_complex select { + max-inline-size: 100%; + min-inline-size: 100px; + inline-size: auto; + } + +/*# sourceMappingURL=theme-framework.css.map */ diff --git a/assets/css/dist/theme-framework.min.css b/assets/css/dist/theme-framework.min.css new file mode 100644 index 0000000..2437dfa --- /dev/null +++ b/assets/css/dist/theme-framework.min.css @@ -0,0 +1 @@ +.gform-theme--framework .gfield_chainedselect.vertical.ginput_complex select{inline-size:auto;max-inline-size:100%;min-inline-size:100px} \ No newline at end of file diff --git a/assets/css/dist/theme.css b/assets/css/dist/theme.css new file mode 100644 index 0000000..a27b05b --- /dev/null +++ b/assets/css/dist/theme.css @@ -0,0 +1,60 @@ +/* +---------------------------------------------------------------- + +theme.css +Gravity Forms Gravity Theme Styles +For the Chained Selects Add-On +A light theme for the frontend engineered to get reasonably +nice look and feel in all our standard theme targets. +https://www.gravityforms.com + +Theme dependencies: +- Gravity Forms Basic Theme: basic.css + +Gravity Forms is a Rocketgenius project +copyright 2008-2022 Rocketgenius Inc. +https://www.rocketgenius.com +this may not be re-distributed without the +express written permission of the author. + +NOTE: DO NOT EDIT THIS FILE! +THIS FILE IS REPLACED DURING AUTO UPGRADE +AND ANY CHANGES MADE HERE WILL BE OVERWRITTEN. + +---------------------------------------------------------------- +*/ + +.gravity-theme.gform_wrapper .gfield_chainedselect .gf_chain_complete, .gform_legacy_markup_wrapper.gform_wrapper .gfield_chainedselect .gf_chain_complete { + background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTc5MiIgaGVpZ2h0PSIxNzkyIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPiA8Zz4gPHBhdGggZD0ibTE2NzEsNTY2cTAsNDAgLTI4LDY4bC03MjQsNzI0bC0xMzYsMTM2cS0yOCwyOCAtNjgsMjh0LTY4LC0yOGwtMTM2LC0xMzZsLTM2MiwtMzYycS0yOCwtMjggLTI4LC02OHQyOCwtNjhsMTM2LC0xMzZxMjgsLTI4IDY4LC0yOHQ2OCwyOGwyOTQsMjk1bDY1NiwtNjU3cTI4LC0yOCA2OCwtMjh0NjgsMjhsMTM2LDEzNnEyOCwyOCAyOCw2OHoiIGZpbGw9IiMwMDAwMDAiLz4gPC9nPiA8L3N2Zz4=); + background-size: 16px; + display: inline-block; + opacity: 0.5; + vertical-align: middle; + inline-size: 16px; + } + +.gravity-theme.gform_wrapper .gfield_chainedselect span, .gform_legacy_markup_wrapper.gform_wrapper .gfield_chainedselect span { + display: inline-block; + flex: none; + padding-block: 0; + padding-inline: 4px 0; + } + +.gravity-theme.gform_wrapper .gfield_chainedselect.vertical.ginput_complex, .gform_legacy_markup_wrapper.gform_wrapper .gfield_chainedselect.vertical.ginput_complex { + display: block; + } + +.gravity-theme.gform_wrapper .gfield_chainedselect.vertical.ginput_complex span, .gform_legacy_markup_wrapper.gform_wrapper .gfield_chainedselect.vertical.ginput_complex span { + display: block; + padding-block: 0 4px !important; + padding-inline: 0 !important; + inline-size: 100%; + } + +.gravity-theme.gform_wrapper .gfield_chainedselect.vertical.ginput_complex select, .gform_legacy_markup_wrapper.gform_wrapper .gfield_chainedselect.vertical.ginput_complex select { + max-inline-size: 100%; + min-inline-size: 100px; + inline-size: auto; + } + +/*# sourceMappingURL=theme.css.map */ diff --git a/assets/css/dist/theme.min.css b/assets/css/dist/theme.min.css new file mode 100644 index 0000000..faef881 --- /dev/null +++ b/assets/css/dist/theme.min.css @@ -0,0 +1 @@ +.gform_legacy_markup_wrapper.gform_wrapper .gfield_chainedselect .gf_chain_complete,.gravity-theme.gform_wrapper .gfield_chainedselect .gf_chain_complete{background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTc5MiIgaGVpZ2h0PSIxNzkyIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGQ9Ik0xNjcxIDU2NnEwIDQwLTI4IDY4bC03MjQgNzI0LTEzNiAxMzZxLTI4IDI4LTY4IDI4dC02OC0yOGwtMTM2LTEzNi0zNjItMzYycS0yOC0yOC0yOC02OHQyOC02OGwxMzYtMTM2cTI4LTI4IDY4LTI4dDY4IDI4bDI5NCAyOTUgNjU2LTY1N3EyOC0yOCA2OC0yOHQ2OCAyOGwxMzYgMTM2cTI4IDI4IDI4IDY4eiIvPjwvc3ZnPg==);background-size:16px;display:inline-block;inline-size:16px;opacity:.5;vertical-align:middle}.gform_legacy_markup_wrapper.gform_wrapper .gfield_chainedselect span,.gravity-theme.gform_wrapper .gfield_chainedselect span{display:inline-block;flex:none;padding-block:0;padding-inline:4px 0}.gform_legacy_markup_wrapper.gform_wrapper .gfield_chainedselect.vertical.ginput_complex,.gravity-theme.gform_wrapper .gfield_chainedselect.vertical.ginput_complex{display:block}.gform_legacy_markup_wrapper.gform_wrapper .gfield_chainedselect.vertical.ginput_complex span,.gravity-theme.gform_wrapper .gfield_chainedselect.vertical.ginput_complex span{display:block;inline-size:100%;padding-block:0 4px!important;padding-inline:0!important}.gform_legacy_markup_wrapper.gform_wrapper .gfield_chainedselect.vertical.ginput_complex select,.gravity-theme.gform_wrapper .gfield_chainedselect.vertical.ginput_complex select{inline-size:auto;max-inline-size:100%;min-inline-size:100px} \ No newline at end of file diff --git a/assets/sample.csv b/assets/sample.csv new file mode 100644 index 0000000..7ea3c20 --- /dev/null +++ b/assets/sample.csv @@ -0,0 +1,1109 @@ +Year,Make,Model +2015,Acura,ILX +2015,Acura,MDX +2015,Acura,RDX +2015,Acura,RLX +2015,Acura,TLX +2015,Alfa Romeo,4C +2015,Alfa Romeo,4C Spider +2015,Aston Martin,V12 Vantage S +2015,Aston Martin,V8 Vantage +2015,Aston Martin,Vanquish +2015,Audi,A3 +2015,Audi,A3 e-tron +2015,Audi,A4 +2015,Audi,A5 +2015,Audi,A6 +2015,Audi,A7 +2015,Audi,A8 +2015,Audi,allroad +2015,Audi,Q3 +2015,Audi,Q5 +2015,Audi,Q7 +2015,Audi,R8 +2015,Audi,RS 5 +2015,Audi,RS 7 +2015,Audi,S3 +2015,Audi,S4 +2015,Audi,S5 +2015,Audi,S6 +2015,Audi,S7 +2015,Audi,S8 +2015,Audi,SQ5 +2015,Audi,TT +2015,Audi,TTS +2015,Bentley,Continental GT +2015,Bentley,Flying Spur +2015,Bentley,Mulsanne +2015,BMW,2 Series +2015,BMW,3 Series +2015,BMW,3 Series Gran Turismo +2015,BMW,4 Series +2015,BMW,4 Series Gran Coupe +2015,BMW,5 Series +2015,BMW,5 Series Gran Turismo +2015,BMW,6 Series +2015,BMW,6 Series Gran Coupe +2015,BMW,7 Series +2015,BMW,ActiveHybrid 5 +2015,BMW,ActiveHybrid 7 +2015,BMW,ALPINA B6 Gran Coupe +2015,BMW,ALPINA B7 +2015,BMW,i3 +2015,BMW,i8 +2015,BMW,M3 +2015,BMW,M4 +2015,BMW,M5 +2015,BMW,M6 +2015,BMW,M6 Gran Coupe +2015,BMW,X1 +2015,BMW,X3 +2015,BMW,X4 +2015,BMW,X5 +2015,BMW,X5 M +2015,BMW,X6 +2015,BMW,X6 M +2015,BMW,Z4 +2015,Buick,Enclave +2015,Buick,Encore +2015,Buick,LaCrosse +2015,Buick,Regal +2015,Buick,Verano +2015,Cadillac,ATS +2015,Cadillac,ATS Coupe +2015,Cadillac,CTS +2015,Cadillac,CTS-V Coupe +2015,Cadillac,Escalade +2015,Cadillac,Escalade ESV +2015,Cadillac,SRX +2015,Cadillac,XTS +2015,Chevrolet,Camaro +2015,Chevrolet,City Express +2015,Chevrolet,Colorado +2015,Chevrolet,Corvette +2015,Chevrolet,Cruze +2015,Chevrolet,Equinox +2015,Chevrolet,Express +2015,Chevrolet,Express Cargo +2015,Chevrolet,Impala +2015,Chevrolet,Impala Limited +2015,Chevrolet,Malibu +2015,Chevrolet,Silverado 1500 +2015,Chevrolet,Silverado 2500HD +2015,Chevrolet,Silverado 3500HD +2015,Chevrolet,Sonic +2015,Chevrolet,Spark +2015,Chevrolet,Spark EV +2015,Chevrolet,SS +2015,Chevrolet,Suburban +2015,Chevrolet,Tahoe +2015,Chevrolet,Traverse +2015,Chevrolet,Trax +2015,Chevrolet,Volt +2015,Chrysler,200 +2015,Chrysler,300 +2015,Chrysler,Town and Country +2015,Dodge,Challenger +2015,Dodge,Charger +2015,Dodge,Dart +2015,Dodge,Durango +2015,Dodge,Grand Caravan +2015,Dodge,Journey +2015,Dodge,Viper +2015,Ferrari,458 Italia +2015,Ferrari,California T +2015,Ferrari,F12 Berlinetta +2015,Ferrari,FF +2015,Ferrari,FXX K +2015,FIAT,500 +2015,FIAT,500e +2015,FIAT,500L +2015,Ford,C-Max Energi +2015,Ford,C-Max Hybrid +2015,Ford,Edge +2015,Ford,Escape +2015,Ford,Expedition +2015,Ford,Explorer +2015,Ford,F-150 +2015,Ford,F-250 Super Duty +2015,Ford,F-350 Super Duty +2015,Ford,F-450 Super Duty +2015,Ford,Fiesta +2015,Ford,Flex +2015,Ford,Focus +2015,Ford,Focus ST +2015,Ford,Fusion +2015,Ford,Fusion Energi +2015,Ford,Fusion Hybrid +2015,Ford,Mustang +2015,Ford,Taurus +2015,Ford,Transit Connect +2015,Ford,Transit Van +2015,Ford,Transit Wagon +2015,GMC,Acadia +2015,GMC,Canyon +2015,GMC,Canyon Nightfall Edition +2015,GMC,Savana +2015,GMC,Savana Cargo +2015,GMC,Sierra 1500 +2015,GMC,Sierra 2500HD +2015,GMC,Sierra 3500HD +2015,GMC,Terrain +2015,GMC,Yukon +2015,GMC,Yukon XL +2015,Honda,Accord +2015,Honda,Accord Hybrid +2015,Honda,Civic +2015,Honda,CR-V +2015,Honda,CR-Z +2015,Honda,Crosstour +2015,Honda,Fit +2015,Honda,Odyssey +2015,Honda,Pilot +2015,Hyundai,2016 Tucson Fuel Cell +2015,Hyundai,Accent +2015,Hyundai,Azera +2015,Hyundai,Elantra +2015,Hyundai,Elantra GT +2015,Hyundai,Equus +2015,Hyundai,Genesis +2015,Hyundai,Genesis Coupe +2015,Hyundai,Santa Fe +2015,Hyundai,Santa Fe Sport +2015,Hyundai,Sonata +2015,Hyundai,Sonata Hybrid +2015,Hyundai,Tucson +2015,Hyundai,Veloster +2015,Infiniti,Q40 +2015,Infiniti,Q50 +2015,Infiniti,Q60 Convertible +2015,Infiniti,Q60 Coupe +2015,Infiniti,Q70 +2015,Infiniti,QX50 +2015,Infiniti,QX60 +2015,Infiniti,QX70 +2015,Infiniti,QX80 +2015,Jaguar,F-TYPE +2015,Jaguar,XF +2015,Jaguar,XJ +2015,Jaguar,XK +2015,Jeep,Cherokee +2015,Jeep,Compass +2015,Jeep,Grand Cherokee +2015,Jeep,Grand Cherokee SRT +2015,Jeep,Patriot +2015,Jeep,Renegade +2015,Jeep,Wrangler +2015,Kia,Cadenza +2015,Kia,Forte +2015,Kia,K900 +2015,Kia,Optima +2015,Kia,Optima Hybrid +2015,Kia,Rio +2015,Kia,Sedona +2015,Kia,Sorento +2015,Kia,Soul +2015,Kia,Soul EV +2015,Kia,Sportage +2015,Lamborghini,Aventador +2015,Lamborghini,Huracan +2015,Land Rover,Discovery Sport +2015,Land Rover,LR2 +2015,Land Rover,LR4 +2015,Land Rover,Range Rover +2015,Land Rover,Range Rover Evoque +2015,Land Rover,Range Rover Evoque Convertible +2015,Land Rover,Range Rover Sport +2015,Lexus,CT 200h +2015,Lexus,ES 300h +2015,Lexus,ES 350 +2015,Lexus,GS 350 +2015,Lexus,GS 450h +2015,Lexus,GX 460 +2015,Lexus,IS 250 +2015,Lexus,IS 250 C +2015,Lexus,IS 350 +2015,Lexus,IS 350 C +2015,Lexus,LS 460 +2015,Lexus,LS 600h L +2015,Lexus,LX 570 +2015,Lexus,NX 200t +2015,Lexus,NX 300h +2015,Lexus,RC 350 +2015,Lexus,RC F +2015,Lexus,RX 350 +2015,Lexus,RX 450h +2015,Lincoln,MKC +2015,Lincoln,MKS +2015,Lincoln,MKT +2015,Lincoln,MKX +2015,Lincoln,MKZ +2015,Lincoln,Navigator +2015,Maserati,Ghibli +2015,Maserati,Ghibli S Q4 Ermenegildo Zegna +2015,Maserati,GranTurismo +2015,Maserati,GranTurismo Convertible +2015,Maserati,Quattroporte +2015,Mazda,3 +2015,Mazda,5 +2015,Mazda,6 +2015,Mazda,CX-5 +2015,Mazda,CX-9 +2015,Mazda,MX-5 Miata +2015,McLaren,650S Coupe +2015,McLaren,650S Spider +2015,McLaren,P1 +2015,Mercedes-Benz,B-Class Electric Drive +2015,Mercedes-Benz,C-Class +2015,Mercedes-Benz,CLA-Class +2015,Mercedes-Benz,CLS-Class +2015,Mercedes-Benz,E-Class +2015,Mercedes-Benz,G-Class +2015,Mercedes-Benz,GL-Class +2015,Mercedes-Benz,GLA-Class +2015,Mercedes-Benz,GLK-Class +2015,Mercedes-Benz,M-Class +2015,Mercedes-Benz,S-Class +2015,Mercedes-Benz,SL-Class +2015,Mercedes-Benz,SLK-Class +2015,Mercedes-Benz,SLS AMG GT Final Edition +2015,Mercedes-Benz,Sprinter +2015,MINI,Cooper +2015,MINI,Cooper Countryman +2015,MINI,Cooper Coupe +2015,MINI,Cooper Paceman +2015,MINI,Cooper Roadster +2015,MINI,John Cooper Works Hardtop +2015,Mitsubishi,Lancer +2015,Mitsubishi,Lancer Evolution +2015,Mitsubishi,Mirage +2015,Mitsubishi,Outlander +2015,Mitsubishi,Outlander Sport +2015,Nissan,370Z +2015,Nissan,Altima +2015,Nissan,Armada +2015,Nissan,Frontier +2015,Nissan,GT-R +2015,Nissan,Leaf +2015,Nissan,Murano +2015,Nissan,NV Cargo +2015,Nissan,NV Passenger +2015,Nissan,NV200 +2015,Nissan,Pathfinder +2015,Nissan,Quest +2015,Nissan,Rogue +2015,Nissan,Rogue Select +2015,Nissan,Sentra +2015,Nissan,Titan +2015,Nissan,Versa +2015,Porsche,911 +2015,Porsche,918 Spyder +2015,Porsche,Boxster +2015,Porsche,Cayenne +2015,Porsche,Cayman +2015,Porsche,Macan +2015,Porsche,Panamera +2015,Ram,1500 +2015,Ram,1500 Rebel +2015,Ram,2500 +2015,Ram,3500 +2015,Ram,CV Tradesman +2015,Ram,Promaster Cargo Van +2015,Ram,Promaster City +2015,Ram,Promaster Window Van +2015,Rolls-Royce,Ghost Series II +2015,Rolls-Royce,Phantom +2015,Rolls-Royce,Phantom Coupe +2015,Rolls-Royce,Phantom Drophead Coupe +2015,Rolls-Royce,Wraith +2015,Scion,FR-S +2015,Scion,iQ +2015,Scion,tC +2015,Scion,xB +2015,smart,fortwo +2015,Subaru,BRZ +2015,Subaru,Forester +2015,Subaru,Impreza +2015,Subaru,Legacy +2015,Subaru,Outback +2015,Subaru,WRX +2015,Subaru,XV Crosstrek +2015,Tesla,Model S +2015,Toyota,4Runner +2015,Toyota,Avalon +2015,Toyota,Avalon Hybrid +2015,Toyota,Camry +2015,Toyota,Camry Hybrid +2015,Toyota,Corolla +2015,Toyota,Highlander +2015,Toyota,Highlander Hybrid +2015,Toyota,Land Cruiser +2015,Toyota,Prius +2015,Toyota,Prius c +2015,Toyota,Prius Plug-in +2015,Toyota,Prius v +2015,Toyota,RAV4 +2015,Toyota,Sequoia +2015,Toyota,Sienna +2015,Toyota,Tacoma +2015,Toyota,Tundra +2015,Toyota,Venza +2015,Toyota,Yaris +2015,Volkswagen,Beetle +2015,Volkswagen,Beetle Convertible +2015,Volkswagen,CC +2015,Volkswagen,e-Golf +2015,Volkswagen,Eos +2015,Volkswagen,Golf +2015,Volkswagen,Golf GTI +2015,Volkswagen,Golf R +2015,Volkswagen,Golf SportWagen +2015,Volkswagen,Passat +2015,Volkswagen,Tiguan +2015,Volkswagen,Touareg +2015,Volvo,S60 +2015,Volvo,S80 +2015,Volvo,V60 +2015,Volvo,V60 Cross Country +2015,Volvo,XC60 +2015,Volvo,XC70 +2016,Acura,ILX +2016,Acura,MDX +2016,Acura,NSX +2016,Acura,RDX +2016,Acura,RLX +2016,Acura,TLX +2016,Alfa Romeo,4C +2016,Alfa Romeo,4C Spider +2016,Aston Martin,DB9 GT +2016,Aston Martin,Rapide S +2016,Aston Martin,V12 Vantage S +2016,Aston Martin,V8 Vantage +2016,Aston Martin,Vanquish +2016,Audi,A3 +2016,Audi,A4 +2016,Audi,A5 +2016,Audi,A6 +2016,Audi,A7 +2016,Audi,A8 +2016,Audi,allroad +2016,Audi,Q1 +2016,Audi,Q3 +2016,Audi,Q5 +2016,Audi,Q7 +2016,Audi,R8 +2016,Audi,RS +2016,Audi,RS 7 +2016,Audi,S3 +2016,Audi,S4 +2016,Audi,S5 +2016,Audi,S6 +2016,Audi,S7 +2016,Audi,S8 +2016,Audi,SQ5 +2016,Audi,TT +2016,Audi,TTS +2016,Bentley,Continental GT +2016,Bentley,Continental GT Speed +2016,Bentley,Flying Spur +2016,Bentley,Mulsanne +2016,BMW,2 Series +2016,BMW,3 Series +2016,BMW,3 Series Gran Turismo +2016,BMW,4 Series +2016,BMW,4 Series Gran Coupe +2016,BMW,5 Series +2016,BMW,5 Series Gran Turismo +2016,BMW,6 Series +2016,BMW,6 Series Gran Coupe +2016,BMW,7 Series +2016,BMW,ActiveHybrid 5 +2016,BMW,ALPINA B6 Gran Coupe +2016,BMW,i3 +2016,BMW,i8 +2016,BMW,M2 +2016,BMW,M3 +2016,BMW,M4 +2016,BMW,M4 GTS +2016,BMW,M5 +2016,BMW,M6 +2016,BMW,M6 Gran Coupe +2016,BMW,X1 +2016,BMW,X3 +2016,BMW,X4 +2016,BMW,X5 +2016,BMW,X5 M +2016,BMW,X6 +2016,BMW,X6 M +2016,BMW,Z4 +2016,Buick,Cascada +2016,Buick,Enclave +2016,Buick,Encore +2016,Buick,Envision +2016,Buick,LaCrosse +2016,Buick,Regal +2016,Buick,Verano +2016,Cadillac,ATS +2016,Cadillac,ATS Coupe +2016,Cadillac,ATS-V +2016,Cadillac,CT6 +2016,Cadillac,CTS +2016,Cadillac,CTS-V +2016,Cadillac,ELR +2016,Cadillac,Escalade +2016,Cadillac,Escalade ESV +2016,Cadillac,SRX +2016,Cadillac,XTS +2016,Chevrolet,Camaro +2016,Chevrolet,City Express +2016,Chevrolet,Colorado +2016,Chevrolet,Corvette +2016,Chevrolet,Corvette Stingray +2016,Chevrolet,Cruze +2016,Chevrolet,Equinox +2016,Chevrolet,Express +2016,Chevrolet,Express Cargo +2016,Chevrolet,Impala +2016,Chevrolet,Impala Limited +2016,Chevrolet,Malibu +2016,Chevrolet,Malibu Hybrid +2016,Chevrolet,Malibu Limited +2016,Chevrolet,Silverado 1500 +2016,Chevrolet,Silverado 2500HD +2016,Chevrolet,Silverado 3500HD +2016,Chevrolet,Sonic +2016,Chevrolet,Spark +2016,Chevrolet,SS +2016,Chevrolet,Suburban +2016,Chevrolet,Tahoe +2016,Chevrolet,Traverse +2016,Chevrolet,Trax +2016,Chevrolet,Volt +2016,Chrysler,100 +2016,Chrysler,200 +2016,Chrysler,300 +2016,Chrysler,Town and Country +2016,Dodge,Challenger +2016,Dodge,Charger +2016,Dodge,Dart +2016,Dodge,Durango +2016,Dodge,Grand Caravan +2016,Dodge,Journey +2016,Dodge,Viper +2016,FIAT,500 +2016,FIAT,500e +2016,FIAT,500L +2016,FIAT,500X +2016,Ford,C-Max Energi +2016,Ford,C-Max Hybrid +2016,Ford,Edge +2016,Ford,Escape +2016,Ford,Expedition +2016,Ford,Expedition EL +2016,Ford,Explorer +2016,Ford,Explorer Sport +2016,Ford,F-150 +2016,Ford,F-250 Super Duty +2016,Ford,F-350 Super Duty +2016,Ford,F-450 Super Duty +2016,Ford,Fiesta +2016,Ford,Flex +2016,Ford,Focus +2016,Ford,Focus ST +2016,Ford,Fusion +2016,Ford,Fusion Energi +2016,Ford,Fusion Hybrid +2016,Ford,Mustang +2016,Ford,Ranger +2016,Ford,Shelby GT350 +2016,Ford,Taurus +2016,Ford,Transit Connect +2016,Ford,Transit Van +2016,Ford,Transit Wagon +2016,GMC,Acadia +2016,GMC,Canyon +2016,GMC,Savana +2016,GMC,Savana Cargo +2016,GMC,Sierra +2016,GMC,Sierra 2500HD +2016,GMC,Sierra 3500HD +2016,GMC,Yukon +2016,GMC,Yukon Denali +2016,Honda,Accord +2016,Honda,Civic +2016,Honda,CR-V +2016,Honda,CR-Z +2016,Honda,Fit +2016,Honda,HR-V +2016,Honda,Odyssey +2016,Honda,Pilot +2016,Hyundai,Accent +2016,Hyundai,Azera +2016,Hyundai,Elantra +2016,Hyundai,Equus +2016,Hyundai,Genesis +2016,Hyundai,Genesis Coupe +2016,Hyundai,Santa Fe +2016,Hyundai,Santa Fe Sport +2016,Hyundai,Sonata +2016,Hyundai,Sonata Hybrid +2016,Hyundai,Tucson +2016,Hyundai,Veloster +2016,Infiniti,Q30 +2016,Infiniti,Q50 +2016,Infiniti,Q70 +2016,Infiniti,QX50 +2016,Infiniti,QX60 +2016,Infiniti,QX70 +2016,Infiniti,QX80 +2016,Jaguar,F-TYPE +2016,Jaguar,XF +2016,Jaguar,XJ +2016,Jeep,Cherokee +2016,Jeep,Compass +2016,Jeep,Grand Cherokee +2016,Jeep,Patriot +2016,Jeep,Renegade +2016,Jeep,Wrangler +2016,Kia,Cadenza +2016,Kia,Forte +2016,Kia,K900 +2016,Kia,Optima +2016,Kia,Optima Hybrid +2016,Kia,Rio +2016,Kia,Sedona +2016,Kia,Sorento +2016,Kia,Soul +2016,Kia,Soul EV +2016,Kia,Sportage +2016,Lamborghini,Aventador +2016,Lamborghini,Huracan +2016,Land Rover,Discovery Sport +2016,Land Rover,LR4 +2016,Land Rover,Range Rover +2016,Land Rover,Range Rover Evoque +2016,Land Rover,Range Rover Sport +2016,Lexus,CT 200h +2016,Lexus,ES 300h +2016,Lexus,ES 350 +2016,Lexus,GS 200t +2016,Lexus,GS 350 +2016,Lexus,GS 450h +2016,Lexus,GS F +2016,Lexus,GX 460 +2016,Lexus,IS 200t +2016,Lexus,IS 300 +2016,Lexus,IS 350 +2016,Lexus,LS 460 +2016,Lexus,LS 600h L +2016,Lexus,LX 570 +2016,Lexus,NX 200t +2016,Lexus,NX 300h +2016,Lexus,RC 200t +2016,Lexus,RC 300 +2016,Lexus,RC 350 +2016,Lexus,RC F +2016,Lexus,RX 350 +2016,Lexus,RX 450h +2016,Lincoln,MKC +2016,Lincoln,MKS +2016,Lincoln,MKT +2016,Lincoln,MKX +2016,Lincoln,MKZ +2016,Lincoln,MKZ Hybrid +2016,Lincoln,Navigator +2016,Lincoln,Navigator L +2016,Maserati,Ghibli +2016,Maserati,Ghibli S +2016,Maserati,GranTurismo +2016,Maserati,GranTurismo Convertible +2016,Maserati,Quattroporte +2016,Mazda,2 +2016,Mazda,3 +2016,Mazda,6 +2016,Mazda,CX-3 +2016,Mazda,CX-5 +2016,Mazda,CX-9 +2016,Mazda,MX-5 Miata +2016,McLaren,570S +2016,Mercedes-Benz,AMG GT +2016,Mercedes-Benz,B-Class Electric Drive +2016,Mercedes-Benz,C-Class +2016,Mercedes-Benz,C350 Plug-in Hybrid +2016,Mercedes-Benz,C450 AMG 4MATIC +2016,Mercedes-Benz,CLA-Class +2016,Mercedes-Benz,CLS-Class +2016,Mercedes-Benz,E-Class +2016,Mercedes-Benz,G-Class +2016,Mercedes-Benz,GL-Class +2016,Mercedes-Benz,GLA-Class +2016,Mercedes-Benz,GLC-Class +2016,Mercedes-Benz,GLE-Class +2016,Mercedes-Benz,GLE-Class Coupe +2016,Mercedes-Benz,GLE350d +2016,Mercedes-Benz,Metris +2016,Mercedes-Benz,S-Class +2016,Mercedes-Benz,S600 +2016,Mercedes-Benz,SL-Class +2016,Mercedes-Benz,SLK-Class +2016,Mercedes-Benz,Sprinter +2016,Mercedes-Benz,Sprinter Worker +2016,MINI,Cooper +2016,MINI,Cooper Clubman +2016,MINI,Cooper Countryman +2016,MINI,Cooper Paceman +2016,Mitsubishi,i-MiEV +2016,Mitsubishi,Lancer +2016,Mitsubishi,Outlander +2016,Mitsubishi,Outlander Sport +2016,Nissan,370Z +2016,Nissan,Altima +2016,Nissan,Frontier +2016,Nissan,GT-R +2016,Nissan,Juke +2016,Nissan,Leaf +2016,Nissan,Maxima +2016,Nissan,Murano +2016,Nissan,NV Cargo +2016,Nissan,NV Passenger +2016,Nissan,NV200 +2016,Nissan,Pathfinder +2016,Nissan,Quest +2016,Nissan,Sentra +2016,Nissan,Titan XD +2016,Porsche,911 +2016,Porsche,Boxster +2016,Porsche,Cayenne +2016,Porsche,Cayenne S +2016,Porsche,Cayman +2016,Porsche,Macan +2016,Porsche,Panamera +2016,Ram,1500 +2016,Ram,2500 +2016,Ram,3500 +2016,Ram,Promaster Cargo Van +2016,Ram,Promaster City +2016,Ram,Promaster Window Van +2016,Rolls-Royce,Dawn +2016,Rolls-Royce,Ghost Series II +2016,Rolls-Royce,Phantom +2016,Rolls-Royce,Phantom Coupe +2016,Rolls-Royce,Phantom Drophead Coupe +2016,Rolls-Royce,Wraith +2016,Scion,FR-S +2016,Scion,iA +2016,Scion,iM +2016,Scion,tC +2016,smart,fortwo +2016,Subaru,BRZ +2016,Subaru,Crosstrek +2016,Subaru,Forester +2016,Subaru,Impreza +2016,Subaru,Legacy +2016,Subaru,Outback +2016,Subaru,WRX +2016,Tesla,Model S +2016,Tesla,Model X +2016,Toyota,4Runner +2016,Toyota,Avalon +2016,Toyota,Avalon Hybrid +2016,Toyota,Camry +2016,Toyota,Camry Hybrid +2016,Toyota,Corolla +2016,Toyota,Highlander +2016,Toyota,Highlander Hybrid +2016,Toyota,Land +2016,Toyota,Mirai +2016,Toyota,Prius +2016,Toyota,Prius c +2016,Toyota,Prius v +2016,Toyota,RAV4 +2016,Toyota,RAV4 Hybrid +2016,Toyota,Sequoia +2016,Toyota,Sienna +2016,Toyota,Tacoma +2016,Toyota,Tundra +2016,Toyota,Yaris +2016,Volkswagen,Beetle +2016,Volkswagen,Beetle Convertible +2016,Volkswagen,CC +2016,Volkswagen,e-Golf +2016,Volkswagen,Eos +2016,Volkswagen,Golf +2016,Volkswagen,Golf GTI +2016,Volkswagen,Golf SportWagen +2016,Volkswagen,Jetta +2016,Volkswagen,Jetta Hybrid +2016,Volkswagen,Passat +2016,Volkswagen,Tiguan +2016,Volkswagen,Touareg +2016,Volvo,S60 +2016,Volvo,S80 +2016,Volvo,V60 +2016,Volvo,V60 Cross Country +2016,Volvo,XC60 +2016,Volvo,XC70 +2016,Volvo,XC90 +2016,Volvo,XC90 T8 Plug-in +2017,Acura,ILX +2017,Acura,MDX +2017,Acura,NSX +2017,Acura,RDX +2017,Acura,RLX +2017,Acura,TLX +2017,Alfa Romeo,4C +2017,Alfa Romeo,Giulia +2017,Aston Martin,DB11 +2017,Aston Martin,Rapide S +2017,Aston Martin,V12 Vantage S +2017,Aston Martin,Vanquish +2017,Audi,A3 +2017,Audi,A3 Sportback e-tron +2017,Audi,A4 +2017,Audi,A5 +2017,Audi,A6 +2017,Audi,A7 +2017,Audi,A8 +2017,Audi,allroad +2017,Audi,Q3 +2017,Audi,Q5 +2017,Audi,Q7 +2017,Audi,R8 +2017,Audi,RS +2017,Audi,S3 +2017,Audi,S5 +2017,Audi,S6 +2017,Audi,S7 +2017,Audi,S8 +2017,Audi,SQ5 +2017,Audi,TT +2017,Audi,TTS +2017,Bentley,Bentayga +2017,Bentley,Continental GT +2017,Bentley,Continental GT Speed +2017,Bentley,Continental Supersports +2017,Bentley,Flying Spur +2017,Bentley,Mulsanne +2017,BMW,2 Series +2017,BMW,3 Series +2017,BMW,4 Series +2017,BMW,5 Series +2017,BMW,5 Series Gran Turismo +2017,BMW,6 Series +2017,BMW,7 Series +2017,BMW,ALPINA B6 Gran Coupe +2017,BMW,ALPINA B7 +2017,BMW,i3 +2017,BMW,i8 +2017,BMW,M2 +2017,BMW,M3 +2017,BMW,M4 +2017,BMW,M6 +2017,BMW,M6 Gran Coupe +2017,BMW,X1 +2017,BMW,X3 +2017,BMW,X4 +2017,BMW,X5 +2017,BMW,X6 +2017,Buick,Cascada +2017,Buick,Enclave +2017,Buick,Encore +2017,Buick,Envision +2017,Buick,LaCrosse +2017,Buick,Regal +2017,Buick,Verano +2017,Cadillac,ATS +2017,Cadillac,ATS Coupe +2017,Cadillac,ATS-V +2017,Cadillac,ATS-V Coupe +2017,Cadillac,CT6 +2017,Cadillac,CTS +2017,Cadillac,CTS-V +2017,Cadillac,Escalade +2017,Cadillac,Escalade ESV +2017,Cadillac,XT5 +2017,Cadillac,XTS +2017,Chevrolet,Bolt +2017,Chevrolet,Camaro +2017,Chevrolet,Colorado +2017,Chevrolet,Corvette +2017,Chevrolet,Cruze +2017,Chevrolet,Equinox +2017,Chevrolet,Express +2017,Chevrolet,Express Cargo +2017,Chevrolet,Express LS 3500 +2017,Chevrolet,Express LT 3500 +2017,Chevrolet,Impala +2017,Chevrolet,Malibu +2017,Chevrolet,Malibu Hybrid +2017,Chevrolet,Silverado 1500 +2017,Chevrolet,Silverado 2500HD +2017,Chevrolet,Silverado 3500HD +2017,Chevrolet,Spark +2017,Chevrolet,SS +2017,Chevrolet,Suburban +2017,Chevrolet,Tahoe +2017,Chevrolet,Traverse +2017,Chevrolet,Trax +2017,Chevrolet,Volt +2017,Chrysler,200 +2017,Chrysler,300 +2017,Chrysler,Pacifica +2017,Dodge,Challenger +2017,Dodge,Charger +2017,Dodge,Durango +2017,Dodge,Grand Caravan +2017,Dodge,Journey +2017,Dodge,Viper +2017,FIAT,124 Spider +2017,FIAT,500 +2017,FIAT,500e +2017,FIAT,500L +2017,FIAT,500X +2017,Ford,C-Max Energi +2017,Ford,C-Max Hybrid +2017,Ford,Edge +2017,Ford,Escape +2017,Ford,Expedition +2017,Ford,Expedition EL +2017,Ford,Explorer +2017,Ford,F-150 +2017,Ford,F-250 Super Duty +2017,Ford,F-350 +2017,Ford,F-350 Super Duty +2017,Ford,F-450 Super Duty +2017,Ford,Fiesta +2017,Ford,Flex +2017,Ford,Focus +2017,Ford,Fusion +2017,Ford,Fusion Energi +2017,Ford,Fusion Hybrid +2017,Ford,GT +2017,Ford,Shelby GT350 +2017,Ford,Taurus +2017,Ford,Transit Connect +2017,Ford,Transit Van +2017,Ford,Transit Wagon +2017,Genesis,G80 +2017,Genesis,G90 +2017,GMC,Canyon +2017,GMC,Savana Cargo +2017,GMC,Sierra 1500 +2017,GMC,Sierra 3500HD +2017,GMC,Terrain +2017,GMC,Yukon +2017,Honda,Accord +2017,Honda,Accord Hybrid +2017,Honda,Civic +2017,Honda,Clarity +2017,Honda,CR-V +2017,Honda,Fit +2017,Honda,HR-V +2017,Honda,Odyssey +2017,Honda,Pilot +2017,Honda,Ridgel +2017,Honda,Ridgeline +2017,Hyundai,Accent +2017,Hyundai,Azera +2017,Hyundai,Elantra +2017,Hyundai,Elantra GT +2017,Hyundai,Ioniq +2017,Hyundai,Santa Fe +2017,Hyundai,Santa Fe Sport +2017,Hyundai,Sonata +2017,Hyundai,Sonata Hybrid +2017,Hyundai,Tucson +2017,Hyundai,Veloster +2017,Infiniti,Q50 +2017,Infiniti,Q60 Coupe +2017,Infiniti,Q70 +2017,Infiniti,QX30 +2017,Infiniti,QX50 +2017,Infiniti,QX60 +2017,Infiniti,QX70 +2017,Infiniti,QX80 +2017,Jaguar,F-PACE +2017,Jaguar,F-TYPE +2017,Jaguar,XE +2017,Jaguar,XF +2017,Jaguar,XJ +2017,Jeep,Cherokee +2017,Jeep,Compass +2017,Jeep,Grand Cherokee +2017,Jeep,Patriot +2017,Jeep,Renegade +2017,Jeep,Wrangler +2017,Kia,Cadenza +2017,Kia,Forte +2017,Kia,K900 +2017,Kia,Niro +2017,Kia,Optima +2017,Kia,Optima Hybrid +2017,Kia,Rio +2017,Kia,Sedona +2017,Kia,Sorento +2017,Kia,Soul +2017,Kia,Soul EV +2017,Kia,Sportage +2017,Lamborghini,Aventador +2017,Lamborghini,Huracan +2017,Land Rover,Discovery +2017,Land Rover,Discovery Sport +2017,Land Rover,Range Rover +2017,Land Rover,Range Rover Evoque +2017,Land Rover,Range Rover Sport +2017,Lexus,CT 200h +2017,Lexus,ES 300h +2017,Lexus,ES 350 +2017,Lexus,GS 200t +2017,Lexus,GS 350 +2017,Lexus,GS 450h +2017,Lexus,GS F +2017,Lexus,GX 460 +2017,Lexus,IS 200t +2017,Lexus,IS 300 +2017,Lexus,IS 350 +2017,Lexus,LS 460 +2017,Lexus,LX 570 +2017,Lexus,NX 200t +2017,Lexus,NX 300h +2017,Lexus,RC 200t +2017,Lexus,RC 300 +2017,Lexus,RC 350 +2017,Lexus,RC F +2017,Lexus,RX 350 +2017,Lexus,RX 450h +2017,Lincoln,Continental +2017,Lincoln,MKC +2017,Lincoln,MKT +2017,Lincoln,MKX +2017,Lincoln,MKZ +2017,Lincoln,Navigator +2017,Lincoln,Navigator L +2017,Lotus,Evora +2017,Maserati,Ghibli +2017,Maserati,Ghibli S Q4 +2017,Maserati,GranTurismo +2017,Maserati,Levante +2017,Maserati,Quattroporte +2017,Mazda,3 +2017,Mazda,6 +2017,Mazda,CX-3 +2017,Mazda,CX-9 +2017,Mazda,MX-5 Miata +2017,McLaren,570GT +2017,McLaren,570S +2017,Mercedes-Benz,B-Class +2017,Mercedes-Benz,C-Class +2017,Mercedes-Benz,CLA-Class +2017,Mercedes-Benz,CLS-Class +2017,Mercedes-Benz,E-Class +2017,Mercedes-Benz,G-Class +2017,Mercedes-Benz,GLA-Class +2017,Mercedes-Benz,GLC-Class +2017,Mercedes-Benz,GLC-Class Coupe +2017,Mercedes-Benz,GLE-Class +2017,Mercedes-Benz,GLE-Class Coupe +2017,Mercedes-Benz,GLS-Class +2017,Mercedes-Benz,Metris +2017,Mercedes-Benz,S-Class +2017,Mercedes-Benz,S550 +2017,Mercedes-Benz,S600 +2017,Mercedes-Benz,SL-Class +2017,Mercedes-Benz,SLC-Class +2017,MINI,Clubman +2017,MINI,Convertible +2017,MINI,Countryman +2017,MINI,Hardtop +2017,Mitsubishi,i-MiEV +2017,Mitsubishi,Lancer +2017,Mitsubishi,Mirage +2017,Mitsubishi,Mirage G4 +2017,Mitsubishi,Outlander +2017,Mitsubishi,Outlander Sport +2017,Nissan,Altima +2017,Nissan,Armada +2017,Nissan,Frontier +2017,Nissan,GT-R +2017,Nissan,Juke +2017,Nissan,Leaf +2017,Nissan,Maxima +2017,Nissan,Murano +2017,Nissan,NV Cargo +2017,Nissan,NV Passenger +2017,Nissan,NV200 +2017,Nissan,Pathfinder +2017,Nissan,Rogue +2017,Nissan,Sentra +2017,Nissan,Titan +2017,Nissan,Titan XD +2017,Nissan,Versa +2017,Nissan,Versa Note +2017,Porsche,718 +2017,Porsche,911 +2017,Porsche,Cayenne +2017,Porsche,Cayenne S +2017,Porsche,Macan +2017,Porsche,Panamera +2017,Ram,1500 +2017,Ram,2500 +2017,Ram,3500 +2017,Ram,Promaster Cargo Van +2017,Ram,Promaster City +2017,Ram,Promaster Window Van +2017,Rolls-Royce,Dawn +2017,Rolls-Royce,Ghost Series II +2017,Rolls-Royce,Phantom +2017,Rolls-Royce,Wraith +2017,smart,fortwo +2017,Subaru,BRZ +2017,Subaru,Crosstrek +2017,Subaru,Forester +2017,Subaru,Impreza +2017,Subaru,Legacy +2017,Subaru,Outback +2017,Subaru,WRX +2017,Tesla,Model S +2017,Tesla,Model X +2017,Toyota,4Runner +2017,Toyota,86 +2017,Toyota,Avalon +2017,Toyota,Avalon Hybrid +2017,Toyota,Camry +2017,Toyota,Camry Hybrid +2017,Toyota,Corolla +2017,Toyota,Corolla iM +2017,Toyota,Highlander +2017,Toyota,Highlander Hybrid +2017,Toyota,Land Cruiser +2017,Toyota,Mirai +2017,Toyota,Prius +2017,Toyota,Prius c +2017,Toyota,Prius Prime +2017,Toyota,RAV4 +2017,Toyota,RAV4 Hybrid +2017,Toyota,Sequoia +2017,Toyota,Sienna +2017,Toyota,Tacoma +2017,Toyota,Tundra +2017,Toyota,Yaris +2017,Toyota,Yaris iA +2017,Volkswagen,Beetle +2017,Volkswagen,Beetle Convertible +2017,Volkswagen,CC +2017,Volkswagen,Golf +2017,Volkswagen,Golf GTI +2017,Volkswagen,Golf R +2017,Volkswagen,Golf SportWagen +2017,Volkswagen,Jetta +2017,Volkswagen,Passat +2017,Volkswagen,Tiguan +2017,Volkswagen,Touareg +2017,Volvo,S60 +2017,Volvo,S60 Cross Country +2017,Volvo,S90 +2017,Volvo,V60 +2017,Volvo,V60 Cross Country +2017,Volvo,V90 Cross Country +2017,Volvo,XC60 +2017,Volvo,XC90 diff --git a/chainedselects.php b/chainedselects.php new file mode 100644 index 0000000..aa07a72 --- /dev/null +++ b/chainedselects.php @@ -0,0 +1,55 @@ +is_gravityforms_supported() && class_exists( 'GF_Field' ) ) { + require_once 'includes/class-gf-field-chainedselect.php'; + } + } + + /** + * Enqueue scripts. + * + * @access public + * @return array $scripts + */ + public function scripts() { + + $min = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG || isset( $_GET['gform_debug'] ) ? '' : '.min'; + + $scripts = array( + array( + 'handle' => 'gform_chained_selects_admin', + 'deps' => array( 'jquery', 'backbone', 'plupload', 'gform_form_admin' ), + 'src' => $this->get_base_url() . "/js/admin{$min}.js", + 'version' => $this->_version, + 'enqueue' => array( + array( 'admin_page' => array( 'form_editor', 'form_settings' ) ), + ), + 'in_footer' => true, + 'callback' => array( $this, 'localize_scripts' ), + ), + array( + 'handle' => 'gform_chained_selects_admin_form_editor', + 'deps' => array( 'jquery', 'backbone', 'gform_form_editor', 'plupload' ), + 'src' => $this->get_base_url() . "/js/admin-form-editor{$min}.js", + 'version' => $this->_version, + 'enqueue' => array( + array( 'admin_page' => array( 'form_editor' ) ), + ), + 'in_footer' => true, + 'callback' => array( $this, 'localize_scripts' ), + ), + array( + 'handle' => 'gform_chained_selects', + 'deps' => array( 'jquery', 'gform_gravityforms' ), + 'src' => $this->get_base_url() . "/js/frontend{$min}.js", + 'version' => $this->_version, + 'enqueue' => array( + array( $this, 'should_enqueue_frontend_script' ) + ), + 'callback' => array( $this, 'localize_scripts' ), + ), + ); + + return array_merge( parent::scripts(), $scripts ); + } + + /** + * Frontend scripts should only be enqueued if we're not on a GF admin page and the form contains our field type. + * + * @param $form + * + * @return bool + */ + public function should_enqueue_frontend_script( $form ) { + return ! GFForms::get_page() && ! rgempty( GFFormsModel::get_fields_by_type( $form, array( 'chainedselect' ) ) ); + } + + /** + * Enqueue styles. + * + * @access public + * @return array $scripts + */ + public function styles() { + + $base_url = $this->get_base_url(); + $min = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG || isset( $_GET['gform_debug'] ) ? '' : '.min'; + + $styles = array( + array( + 'handle' => 'gform_chained_selects_admin', + 'src' => $base_url . "/assets/css/dist/admin{$min}.css", + 'version' => $this->_version, + 'enqueue' => array( + array( 'admin_page' => array( 'form_editor', 'entry_view' ) ), + ), + ), + array( + 'handle' => 'gform_chained_selects_theme', + 'src' => $base_url . "/assets/css/dist/theme{$min}.css", + 'version' => $this->_version, + 'enqueue' => array( + array( $this, 'should_enqueue_frontend_script' ) + ), + ), + ); + + return array_merge( parent::styles(), $styles ); + } + + /** + * An array of styles to enqueue. + * + * @since 1.6 + * + * @param $form + * @param $ajax + * @param $settings + * @param $block_settings + * + * @return array|\string[][] + */ + public function theme_layer_styles( $form, $ajax, $settings, $block_settings = array() ) { + $theme_slug = \GFFormDisplay::get_form_theme_slug( $form ); + + if ( $theme_slug !== 'orbital' ) { + return array(); + } + + $base_url = plugins_url( '', __FILE__ ); + + return array( + 'framework' => array( + array( 'gravity_forms_chainedselects_theme_framework', "$base_url/assets/css/dist/theme-framework.css" ), + ), + ); + } + + public function localize_scripts() { + + wp_localize_script( 'gform_chained_selects_admin', 'gformChainedSelectData', array( + 'defaultChoices' => $this->get_default_choices(), + 'defaultInputs' => $this->get_default_inputs(), + 'fileUploadUrl' => trailingslashit( site_url() ) . '?gf_page=' . GFCommon::get_upload_page_slug(), + 'maxFileSize' => $this->get_max_file_size(), + 'strings' => array( + 'errorProcessingFile' => wp_strip_all_tags( __( 'There was an error processing this file.', 'gravityformschainedselects' ) ), + 'errorUploadingFile' => wp_strip_all_tags( __( 'There was an error uploading this file.', 'gravityformschainedselects' ) ), + 'errorFileType' => wp_strip_all_tags( __( 'Only CSV files are allowed.', 'gravityformschainedselects' ) ), + 'errorFileSize' => sprintf( wp_strip_all_tags( __( 'This file is too big. Max file size is %dMB.', 'gravityformschainedselects' ) ), round( $this->get_max_file_size() / 1000000 ) ), + 'importedFilterFile' => sprintf( wp_strip_all_tags( __( 'This file is imported via %sa filter%s and cannot be modified here.', 'gravityformschainedselects' ) ), '', '' ), + 'errorImportingFilterFile' => sprintf( wp_strip_all_tags( __( 'There was an error importing the file via %sthe filter%s.', 'gravityformschainedselects' ) ), '', '' ), + ) + ) ); + + wp_localize_script( 'gform_chained_selects', 'gformChainedSelectData', array( + 'ajaxUrl' => admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( 'gform_get_next_chained_select_choices' ), + 'spinner' => $this->get_spinner_url(), + 'strings' => array( + 'loading' => wp_strip_all_tags( __( 'Loading', 'gravityformschainedselects' ) ), + 'noOptions' => wp_strip_all_tags( __( 'No options', 'gravityformschainedselects' ) ), + ), + ) ); + + } + + /** + * Returns the URL of the file containing the spinner. + * + * @since 1.6 + * + * @return string + */ + public function get_spinner_url() { + return GFCommon::get_base_url() . '/images/spinner' . ( $this->is_gravityforms_supported( '2.5' ) ? '.svg' : '.gif' ); + } + + public function get_default_choices() { + // ids are set in JS based on newly created field + return array( + array( + 'text' => wp_strip_all_tags( __( 'Parent 1', 'gravityformschainedselects' ) ), + 'value' => wp_strip_all_tags( __( 'Parent 1', 'gravityformschainedselects' ) ), + 'isSelected' => true, + 'choices' => array( + array( + 'text' => wp_strip_all_tags( __( 'Child 1', 'gravityformschainedselects' ) ), + 'value' => wp_strip_all_tags( __( 'Child 1', 'gravityformschainedselects' ) ), + 'isSelected' => true, + ), + array( + 'text' => wp_strip_all_tags( __( 'Child 2', 'gravityformschainedselects' ) ), + 'value' => wp_strip_all_tags( __( 'Child 2', 'gravityformschainedselects' ) ), + 'isSelected' => false, + ), + array( + 'text' => wp_strip_all_tags( __( 'Child 3', 'gravityformschainedselects' ) ), + 'value' => wp_strip_all_tags( __( 'Child 3', 'gravityformschainedselects' ) ), + 'isSelected' => false, + ) + ) + ), + array( + 'text' => wp_strip_all_tags( __( 'Parent 2', 'gravityformschainedselects' ) ), + 'value' => wp_strip_all_tags( __( 'Parent 2', 'gravityformschainedselects' ) ), + 'isSelected' => false, + 'choices' => array( + array( + 'text' => wp_strip_all_tags( __( 'Child 4', 'gravityformschainedselects' ) ), + 'value' => wp_strip_all_tags( __( 'Child 4', 'gravityformschainedselects' ) ), + 'isSelected' => false, + ), + array( + 'text' => wp_strip_all_tags( __( 'Child 5', 'gravityformschainedselects' ) ), + 'value' => wp_strip_all_tags( __( 'Child 5', 'gravityformschainedselects' ) ), + 'isSelected' => false, + ), + array( + 'text' => wp_strip_all_tags( __( 'Child 6', 'gravityformschainedselects' ) ), + 'value' => wp_strip_all_tags( __( 'Child 6', 'gravityformschainedselects' ) ), + 'isSelected' => false, + ) + ) + ), + array( + 'text' => wp_strip_all_tags( __( 'Parent 3', 'gravityformschainedselects' ) ), + 'value' => wp_strip_all_tags( __( 'Parent 3', 'gravityformschainedselects' ) ), + 'isSelected' => false, + 'choices' => array( + array( + 'text' => wp_strip_all_tags( __( 'Child 7', 'gravityformschainedselects' ) ), + 'value' => wp_strip_all_tags( __( 'Child 7', 'gravityformschainedselects' ) ), + 'isSelected' => false, + ), + array( + 'text' => wp_strip_all_tags( __( 'Child 8', 'gravityformschainedselects' ) ), + 'value' => wp_strip_all_tags( __( 'Child 8', 'gravityformschainedselects' ) ), + 'isSelected' => false, + ), + array( + 'text' => wp_strip_all_tags( __( 'Child 9', 'gravityformschainedselects' ) ), + 'value' => wp_strip_all_tags( __( 'Child 9', 'gravityformschainedselects' ) ), + 'isSelected' => false, + ) + ) + ), + ); + } + + public function get_default_inputs() { + return array( + array( + 'label' => wp_strip_all_tags( __( 'Parents', 'gravityformschainedselects' ) ), + 'id' => '', + ), + array( + 'label' => wp_strip_all_tags( __( 'Children', 'gravityformschainedselects' ) ), + 'id' => '', + ) + ); + } + + public function get_max_file_size() { + /** + * Filter the max file size for imported Chained Select files. + * + * @param int $size The max file size in bytes. + * + * @since 1.0 + */ + return apply_filters( 'gform_chainedselects_max_file_size', 1000000 ); // 1mb + } + +} diff --git a/images/menu-icon.svg b/images/menu-icon.svg new file mode 100644 index 0000000..7d47141 --- /dev/null +++ b/images/menu-icon.svg @@ -0,0 +1 @@ +chained selects \ No newline at end of file diff --git a/includes/class-gf-field-chainedselect.php b/includes/class-gf-field-chainedselect.php new file mode 100644 index 0000000..d999916 --- /dev/null +++ b/includes/class-gf-field-chainedselect.php @@ -0,0 +1,927 @@ +is_gravityforms_supported( '2.5-beta-4' ) ? 'gform-icon--chained-selects' : gf_chained_selects()->get_base_url() . '/images/menu-icon.svg'; + } + + /** + * Returns the field's form editor description. + * + * @since 1.4.2 + * + * @return string + */ + public function get_form_editor_field_description() { + return esc_attr__( 'Allows populating drop downs dynamically and chaining multiple drop downs together.', 'gravityformschainedselects' ); + } + + public static function init() { + + add_filter( 'wp_ajax_gform_get_next_chained_select_choices', array( __class__, 'get_next_chained_select_choices' ) ); + add_filter( 'wp_ajax_nopriv_gform_get_next_chained_select_choices', array( __class__, 'get_next_chained_select_choices' ) ); + + add_filter( 'gform_form_post_get_meta', array( __class__, 'maybe_import_from_filter' ) ); + add_filter( 'gform_multifile_upload_field', array( __class__, 'create_custom_file_upload_field' ), 10, 3 ); + add_filter( 'gform_post_multifile_upload', array( __class__, 'import_choices_from_uploaded_file' ), 10, 5 ); + + add_action( 'gform_field_standard_settings', array( __class__, 'output_standard_field_settings_markup' ) ); + add_action( 'gform_field_appearance_settings', array( __class__, 'output_appearance_field_settings_markup' ) ); + add_filter( 'gform_tooltips', array( __class__, 'register_tooltips' ) ); + add_filter( 'gform_is_value_match', array( __class__, 'is_value_match' ), 10, 6 ); + + } + + public static function register_tooltips( $tooltips ) { + $tooltips['gfcs_choices'] = sprintf( '
%s
%s', __( 'Choices', 'gravityformschainedselects' ), __( 'Upload a .csv file to import your choices.', 'gravityformschainedselects' ) ); + $tooltips['gfcs_bulk_add_disabled'] = sprintf( '
%s
%s', __( 'Bulk Add Disabled', 'gravityformschainedselects' ), __( 'Bulk Add is only available for the last Drop Down in a chain. It is best to use Bulk Add for each Drop Down as you build your chain.', 'gravityformschainedselects' ) ); + return $tooltips; + } + + public static function create_custom_file_upload_field( $field, $form, $field_id ) { + + // The $field will be null for a newly created Chained Select field. + $is_valid_field = $field == null || $field->get_input_type() == 'chainedselect'; + if ( ! $is_valid_field ) { + return $field; + } + + $field = new GF_Field_FileUpload( array( + 'id' => $field_id, // $field may be null so always use $field_id + 'multipleFiles' => true, + 'maxFiles' => 1, + 'maxFileSize' => '', + 'allowedExtensions' => 'csv', + 'isChainedSelect' => true, // custom flag used to indicate that this is a custom upload field for a chained select + 'inputs' => rgobj( $field, 'inputs' ), + ) ); + + return $field; + } + + public static function import_choices_from_uploaded_file( $form, $field, $uploaded_filename, $tmp_file_name, $file_path ) { + + // We create a custom file upload field as part of the upload process; check if our custom flag is set. + if ( ! $field->isChainedSelect ) { + return; + } + + if ( ! wp_verify_nonce( rgpost( '_gform_file_upload_nonce_' . $form['id'] ), 'gform_file_upload_' . $form['id'] ) ) { + GFAsyncUpload::die_error( 403, esc_html__( 'Permission denied.', 'gravityforms' ) ); + } + + gf_chained_selects()->log_debug( __METHOD__ . '(): Processing ' . $uploaded_filename ); + $import = self::import_choices( $file_path, $field ); + + if ( is_wp_error( $import ) ) { + gf_chained_selects()->log_error( __METHOD__ . '(): ' . $import->get_error_message() ); + $status_code = rgar( $import->get_error_data( $import->get_error_code() ), 'status_header', 500 ); + GFAsyncUpload::die_error( $status_code, $import->get_error_message() ); + } + + $output = array( + 'status' => 'ok', + 'data' => array( + 'temp_filename' => $tmp_file_name, + 'uploaded_filename' => str_replace( "\\'", "'", urldecode( $uploaded_filename ) ), //Decoding filename to prevent file name mismatch. + 'choices' => $import['choices'], + 'inputs' => $import['inputs'], + ) + ); + + $encoded = json_encode( $output ); + if ( $encoded === false ) { + $json_error = json_last_error_msg(); + gf_chained_selects()->log_error( __METHOD__ . '(): ' . $json_error ); + GFAsyncUpload::die_error( 422, $json_error ); + } + + gf_chained_selects()->log_debug( __METHOD__ . '(): Processing completed.' ); + die( $encoded ); + + } + + public static function import_choices( $path, $field ) { + + if( self::is_choice_limit_exceeded( $path ) ) { + return new WP_Error( 'column_max_exceeded', __( 'One of your columns has exceeded the limit for unique values.', 'gravityformschainedselects' ), array( 'status_header' => 422 ) ); + } + + $choices = array(); + $inputs = array(); + + $handle = fopen( $path, 'r' ); + if( $handle !== false ) { + + while ( ( $row = fgetcsv( $handle, 1000, ',' ) ) !== false ) { + + // filter out empty rows + $row = array_filter( $row, 'strlen' ); + if( empty( $row ) ) { + continue; + } + + // save the headers as inputs + if( empty( $inputs ) ) { + $i = 1; + foreach( $row as $index => $item ) { + if( $i % 10 == 0 ) { + $i++; + } + $inputs[] = array( + 'id' => $field->id . '.' . $i, + 'label' => wp_strip_all_tags( $item ), + 'name' => isset( $field->inputs[ $index ]['name'] ) ? $field->inputs[ $index ]['name'] : '', + ); + $i++; + } + continue; + } + + $parent = null; + + foreach( $row as $item ) { + + if( $parent === null ) { + $parent = &$choices; + } + + if( ! isset( $parent[ $item ] ) ) { + $item = self::sanitize_choice_value( trim( $item ) ); + $parent[ $item ] = array( + 'text' => $item, + 'value' => $item, + 'isSelected' => false, + 'choices' => array() + ); + } + + $parent = &$parent[ $item ]['choices']; + + } + + } + + fclose( $handle ); + + } + + // convert associative array to numeric indexes + self::array_values_recursive( $choices ); + + return compact( 'inputs', 'choices' ); + } + + public static function sanitize_choice_value( $value ) { + $allowed_protocols = wp_allowed_protocols(); + $value = wp_kses_no_null( $value, array( 'slash_zero' => 'keep' ) ); + $value = wp_kses_hook( $value, 'post', $allowed_protocols ); + $value = wp_kses_split( $value, 'post', $allowed_protocols ); + return $value; + } + + public static function is_choice_limit_exceeded( $file_path ) { + + $handle = fopen( $file_path, 'r' ); + if( $handle === false ) { + return null; + } + + $uniques = array(); + $limit = apply_filters( 'gravityformschainedselects_column_unique_values_limit', 5000 ); + $limit = apply_filters( 'gform_chainedselects_column_unique_values_limit', $limit ); + + while ( ( $row = fgetcsv( $handle, 1000, ',' ) ) !== false ) { + + // filter out empty rows + $row = array_filter( $row ); + if( empty( $row ) ) { + continue; + } + + // setup our $uniques based on header + if( empty( $uniques ) ) { + $uniques = array_pad( array(), count( $row ), array() ); + continue; + } + + $parent = null; + + foreach( $row as $column => $item ) { + + if( ! isset( $uniques[ $column ] ) ) { + continue; + } + + if( ! in_array( $item, $uniques[ $column ] ) ) { + $uniques[ $column ][] = $item; + } + + if( count( $uniques[ $column ] ) > $limit ) { + return true; + } + + } + + } + + return false; + } + + public static function array_values_recursive( &$choices, $prop = 'choices' ) { + + $choices = array_values( $choices ); + + for( $i = 0; $i <= count( $choices ); $i++ ) { + if( ! empty( $choices[ $i ][ $prop ] ) ) { + $choices[ $i ][ $prop ] = self::array_values_recursive( $choices[ $i ][ $prop ], $prop ); + } + } + + return $choices; + } + + public static function maybe_import_from_filter( $form ) { + + if ( is_admin() && rgget( 'id' ) != $form['id'] ) { + return $form; + } + + gf_chained_selects()->log_debug( __METHOD__ . '(): running for form #' . $form['id'] ); + + $has_change = false; + + foreach ( $form['fields'] as &$field ) { + + if ( $field->get_input_type() != 'chainedselect' ) { + continue; + } + + gf_chained_selects()->log_debug( __METHOD__ . '(): processing field #' . $field->id ); + + $has_filter = has_filter( 'gform_chainedselects_import_file' ) || has_filter( 'gform_chainedselects_import_file_' . $form['id'] ) || has_filter( 'gform_chainedselects_import_file_' . $form['id'] . '_' . $field->id ); + $has_field_change = ( $has_filter && ! $field->gfcsFilterEnabled ) || ( ! $has_filter && $field->gfcsFilterEnabled ); + + // If filter is set, let's set a flag so we can lock down the field settings UI. + $field->gfcsFilterEnabled = $has_filter; + + if ( ! $has_filter ) { + if ( $has_field_change ) { + $has_change = true; + $field->gfcsFile = null; + $field->gfcsCacheKey = null; + $field->gfcsCacheExpiration = null; + } + gf_chained_selects()->log_debug( __METHOD__ . '(): skipping; filter not used.' ); + continue; + } + + /** + * Provide an import file programmatically. + * + * This import file will override any previously uploaded file via the form settings. + * + * @since 1.0 + * + * @param array $import_file { + * + * An array of details for the file from which choices will be imported. + * + * @var string $url The URL of the file to be imported. + * @var int $expiration The number of seconds until the import file will be re-imported. + * } + */ + $import_details = gf_apply_filters( array( 'gform_chainedselects_import_file', $form['id'], $field->id ), array( + 'url' => '', + 'expiration' => 60 * 60 * 24 + ), $form, $field ); + + if ( ! rgar( $import_details, 'url' ) ) { + gf_chained_selects()->log_debug( __METHOD__ . '(): skipping; empty url.' ); + continue; + } + + $cache_key = implode( '_', array( + sanitize_title_with_dashes( $import_details['url'] ), + intval( $import_details['expiration'] ), + ) ); + + $now = time(); + + // Check if we've recently pinged this URL. + if ( $field->gfcsCacheKey == $cache_key && $now <= $field->gfcsCacheExpiration ) { + gf_chained_selects()->log_debug( sprintf( '%s(): skipping; not expired. now: %d; gfcsCacheExpiration: %d; %d seconds until file can be reimported.', __METHOD__, $now, $field->gfcsCacheExpiration, $field->gfcsCacheExpiration - $now ) ); + continue; + } + + $field->gfcsCacheKey = $cache_key; + $field->gfcsCacheExpiration = time() + $import_details['expiration']; + + $import = self::import_choices_from_remote_file( $import_details['url'], $field, $form ); + + if ( is_wp_error( $import ) ) { + gf_chained_selects()->log_debug( sprintf( '%s(): import failed. %s - %s', __METHOD__, $import->get_error_code(), $import->get_error_message() ) ); + + if ( $field->gfcsFile == null ) { + $field->inputs = gf_chained_selects()->get_default_inputs(); + $field->choices = gf_chained_selects()->get_default_choices(); + } + + // There was an error fetching the file. Let's check the file again in 60 seconds. + $field->gfcsCacheExpiration = time() + 60; + + } else { + + $has_change = true; + + $field->gfcsFile = $import['gfcsFile']; + $field->inputs = $import['inputs']; + $field->choices = $import['choices']; + gf_chained_selects()->log_debug( __METHOD__ . '(): import complete.' ); + + } + + } + + if ( $has_change ) { + remove_filter( 'gform_form_post_get_meta', array( __class__, 'maybe_import_from_filter' ) ); + // Apparently this isn't always set before updating the form? + $form['is_active'] = isset( $form['is_active'] ) ? $form['is_active'] : true; + $result = GFAPI::update_form( $form ); + add_filter( 'gform_form_post_get_meta', array( __class__, 'maybe_import_from_filter' ) ); + if ( is_wp_error( $result ) ) { + gf_chained_selects()->log_debug( sprintf( '%s(): form update failed. %s - %s', __METHOD__, $result->get_error_code(), $result->get_error_message() ) ); + } else { + gf_chained_selects()->log_debug( __METHOD__ . '(): form updated.' ); + } + } else { + gf_chained_selects()->log_debug( __METHOD__ . '(): no change to form.' ); + } + + return $form; + } + + public static function import_choices_from_remote_file( $url, $field, $form ) { + + $upload_dir = GFFormsModel::get_file_upload_path( $form['id'], sprintf( 'gfcs-field-%d-data.csv', $field->id ) ); + $handle = fopen( $upload_dir['path'], 'w+' ); + $response = wp_remote_get( $url ); + $error = false; + + if( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) != 200 ) { + $error = new WP_Error( 'file_inaccessible', __( 'File could not be loaded.', 'gravityformschainedselects' ) ); + } else if( wp_remote_retrieve_header( $response, 'content-type' ) != 'text/csv' ) { + $error = new WP_Error( 'invalid_content_type', __( 'File is not a CSV file.', 'gravityformschainedselects' ) ); + } else if ( empty( wp_remote_retrieve_body( $response ) ) ) { + $error = new WP_Error( 'empty', __( 'File is empty.', 'gravityformschainedselects' ) ); + } + + if( $error ) { + fclose( $handle ); + return $error; + } + + $content = wp_remote_retrieve_body( $response ); + fwrite( $handle, $content, strlen( $content ) ); + + $stats = fstat( $handle ); + fclose( $handle ); + + if( $stats['size'] > gf_chained_selects()->get_max_file_size() ) { + return new WP_Error( 'max_file_size_exceeded', __( 'File is too large.', 'gravityformschainedselects' ) ); + } + + $parsed_url = parse_url( $url ); + $pathinfo = pathinfo( $parsed_url['path'] ); + + $import = self::import_choices( $upload_dir['path'], $field ); + if( is_wp_error( $import ) ) { + return $import; + } + + $import['gfcsFile'] = array( + 'dateUploaded' => time(), + 'name' => $pathinfo['basename'], + 'size' => $stats['size'], + 'type' => 'text/csv', + 'isFromFilter' => true + ); + + return $import; + } + + public static function get_next_chained_select_choices() { + if ( ! wp_verify_nonce( rgpost( 'nonce' ), 'gform_get_next_chained_select_choices' ) ) { + die(); + } + $form_id = rgpost( 'form_id' ); + $field_id = rgpost( 'field_id' ); + $form = GFAPI::get_form( $form_id ); + $field = GFFormsModel::get_field( $form, $field_id ); + $input_id = rgpost( 'input_id' ); + $next_input_id = $field->get_next_input_id( $input_id ); + $value = rgpost( 'value' ); + $choices = $next_input_id ? $field->get_input_choices( $value, $next_input_id ) : array(); + + // Sanitize values before they're sent to frontend script for output. + // We might consider generating the full markup and passing that back but I originally went with passing the + // choices to provide flexibility to the script. + foreach( $choices as &$choice ) { + $choice['value'] = esc_attr( $choice['value'] ); + } + + die( json_encode( $choices ) ); + } + + public static function output_standard_field_settings_markup( $position ) { + + if ( $position != 1362 ) { + return; + } + + ?> + + + + + +
  • + + + +
    +
    +
    + + +
    +
    + get_base_url() . '/assets/sample.csv" target="_blank">', '' ); ?> +
    +
    + +
  • + + + +
  • + + +
  • + +
  • + + + +
  • + + 'advanced_fields', + 'text' => $this->get_form_editor_field_title() + ); + } + + public function get_form_editor_inline_script_on_page_render() { + + $set_default_values = sprintf( ' + function SetDefaultValues_%1$s( field ) { + field.choices = gformChainedSelectData.defaultChoices; + field.inputs = GFCSAdmin.getDefaultInputs( field ); + field.chainedSelectsAlignment = "horizontal"; + field.chainedSelectsHideInactive = false; + return field; + };', + $this->type + ); + + $field_settings = sprintf( ' + ( function( $ ) { + $( document ).bind( "gform_load_field_settings", function( event, field ) { + if( GetInputType( field ) == "%s" ) { + $( "#chained_selects_alignment" ).val( field.chainedSelectsAlignment ); + $( "#chained_selects_hide_inactive" ).prop( "checked", field.chainedSelectsHideInactive ); + } + } ); + } )( jQuery );', + $this->type + ); + + return implode( "\n", array( $set_default_values, $field_settings ) ); + } + + public function validate( $value, $form ) { + if ( ! $this->isRequired ) { + return; + } + // get all + foreach ( $this->inputs as $index => $input ) { + $input_value = rgar( $value, $input['id'] ); + // if no value is provided and there are choices avialable for this field, add a validation error + if ( ! $input_value && ! $this->has_no_options( $value, $input ) ) { + $this->failed_validation = true; + $this->validation_message = empty( $this->errorMessage ) ? __( 'This field is required. Please select a value for each option.', 'gravityformschainedselects' ) : $this->errorMessage; + } + } + } + + public function get_field_input( $form, $value = '', $entry = null ) { + + if ( $this->is_entry_detail() ) { + return $this->get_entry_detail_field_input( $form, $value, $entry ); + } else if( $this->is_form_editor() ) { + // don't populate drop downs with choices in form editor to improve performance + $markup = ''; + foreach ( $this->inputs as $index => $input ) { + $html_id = sprintf( 'input_%d_%s', $form['id'], str_replace( '.', '_', $input['id'] ) ); + $class = 'horizontal' == $this->chainedSelectsAlignment ? 'gform-grid-col--size-auto' : ''; + $markup .= sprintf( + " + + ", + $html_id . '_container', $class, $input['id'], $html_id, $input['label'] + ); + } + return "
    {$markup}
    "; + } + + $form_id = $form['id']; + $is_entry_detail = $this->is_entry_detail(); + $is_form_editor = $this->is_form_editor(); + + $id = $this->id; + $field_id = $is_entry_detail || $is_form_editor || $form_id == 0 ? "input_$id" : 'input_' . $form_id . "_$id"; + $logic_event = gf_chained_selects()->is_gravityforms_supported( '2.4.15.5' ) ? '' : sprintf( 'onchange="gf_input_change( this, %d, %d );"', $form_id, $this->id ); + $disabled_attr = $is_form_editor ? 'disabled="disabled"' : ''; + $markup = ''; + + foreach ( $this->inputs as $index => $input ) { + $html_id = sprintf( 'input_%d_%s', $form_id, str_replace( '.', '_', $input['id'] ) ); + $css_class = $this->has_no_options( $value, $input ) ? 'gf_no_options' : ''; + $css_class .= 'horizontal' == $this->chainedSelectsAlignment ? 'gform-grid-col--size-auto' : ''; + $tabindex = $this->get_tabindex(); + $atts = array( $logic_event, $tabindex, $disabled_attr ); + $input_markup = sprintf( + "", + $input['id'], $html_id, $css_class, implode( ' ', $atts ), $this->get_choices( $value, $input ) + ); + $input_container_markup = sprintf( + " + %s + ", + $html_id . '_container', $css_class, $input_markup + ); + $markup .= $input_container_markup; + } + $markup .= ''; + $field_html_id = sprintf( 'input_%d_%d', $form_id, $this->id ); + $class_suffix = $is_entry_detail ? '_admin' : ''; + $classes = array( + $this->chainedSelectsAlignment ? $this->chainedSelectsAlignment : 'horizontal', + $this->size . $class_suffix, + 'gfield_chainedselect' + ); + $css_class = esc_attr( trim( implode( ' ', $classes ) ) ); + $markup = sprintf( "
    %s
    ", $css_class, $field_html_id, $markup ); + + return $markup; + } + + public function get_entry_detail_field_input( $form, $value, $entry ) { + $markup = '
    '; + foreach ( $this->inputs as $index => $input ) { + $html_id = sprintf( 'input_%d_%s', $form['id'], str_replace( '.', '_', $input['id'] ) ); + $tabindex = $this->get_tabindex(); + $atts = array( $tabindex ); + $input_markup = sprintf( + "", + $input['id'], $html_id, $css_class = '', implode( ' ', $atts ), $value[ $input['id'] ] + ); + $label_markup = sprintf( + "", + $html_id, GFCommon::get_label( $this, $input['id'], true ) + ); + $input_container_markup = sprintf( + " + %s + %s + ", + $html_id . '_container', $css_class = '', $input_markup, $label_markup + ); + $markup .= $input_container_markup; + } + $markup .= '
    '; + + return $markup; + } + + public function get_choices( &$value, $input ) { + + $field = clone $this; + + // temporarily adjust placeholder to current input's label to play nice with GFCommon::get_select_choices() + $field->placeholder = $input['label']; + $field->choices = $this->get_input_choices( $value, $input['id'] ); + + if ( is_array( $field->choices ) ) { + foreach( $field->choices as $choice ) { + if ( rgar( $choice, 'isSelected' ) && ! rgar( $value, $input['id'] ) ) { + $value[ $input['id'] ] = $choice['value']; + } + } + } + + return GFCommon::get_select_choices( $field, rgar( $value, $input['id'], '' ) ); + } + + public function get_input_choices( $chain_value, $input_id = false, $depth = false, $choices = null, $full_chain_value = null ) { + + $full_chain_value = $full_chain_value !== null ? $full_chain_value : $chain_value; + $value = array_shift( $chain_value ); + $index = $input_id ? $this->get_input_index( $input_id ) : 1; // @hack test this for input IDs greater than 10 + $depth = $depth ? $depth : 1; + $choices = $choices === null ? $this->choices : ( empty( $choices ) ? array() : $choices ); + $input_choices = array(); + + if ( $depth % 10 == 0 ) { + $depth ++; + } + + if ( $depth == $index ) { + $input_choices = $choices; + if ( ! $this->is_form_editor() ) { + $input_choices = gf_apply_filters( array( + 'gform_chained_selects_input_choices', + $this->formId, + $this->id, + $index + ), $input_choices, $this->formId, $this, $input_id, $full_chain_value, $value, $index ); + } + } else { + foreach ( $choices as $choice ) { + if ( $choice['value'] == $value ) { + $input_choices = $this->get_input_choices( $chain_value, $input_id, $depth + 1, ! empty( $choice['choices'] ) ? $choice['choices'] : array(), $full_chain_value ); + break; + } + } + } + + if ( empty( $input_choices ) && $this->get_previous_input_value( $input_id, $full_chain_value ) ) { + if ( ! $this->is_form_editor() ) { + $input_choices = gf_apply_filters( array( + 'gform_chained_selects_input_choices', + $this->formId, + $this->id, + $index + ), $input_choices, $this->formId, $this, $input_id, $full_chain_value, $value, $index ); + } + if ( empty( $input_choices ) ) { + $input_choices = array( + array( + 'text' => __( 'No options', 'gravityformschainedselects' ), + 'value' => '', + 'isSelected' => true, + 'noOptions' => true + ) + ); + } + } + + return $input_choices; + } + + public function has_no_options( $value, $input ) { + $choices = $this->get_input_choices( $value, $input['id'] ); + + return rgars( $choices, '0/noOptions' ); + } + + public function get_value_entry_detail( $value, $currency = '', $use_text = false, $format = 'html', $media = 'screen' ) { + + $filtered = is_array( $value ) ? array_filter( $value ) : ''; + if( empty( $filtered ) ) { + return ''; + } + + $return = ''; + + foreach( $this->inputs as $input ) { + if( $format == 'html' ) { + if( $this->is_entry_detail() ) { + $return .= sprintf( '
    %s: %s
    ', $input['label'], $value[ $input['id'] ] ); + } else { + $font_open = $media == 'email' ? '' : ''; + $font_close = $media == 'email' ? '' : ''; + $return .= sprintf( '%1$s%3$s:%2$s%1$s%4$s%2$s', $font_open, $font_close, $input['label'], $value[ $input['id'] ] ); + } + } else { + $return .= sprintf( "%s: %s\n", $input['label'], $value[ $input['id'] ] ); + } + } + + if( $format == 'html' ) { + if( $this->is_entry_detail() ) { + $return = sprintf( '
    %s
    ', $return ); + } else { + $return = sprintf( '%s
    ', $return ); + } + } + + return $return; + } + + public function get_input_property( $input_id, $property_name ) { + $input = GFFormsModel::get_input( $this, $this->id . '.' . (string) $input_id ); + + return rgar( $input, $property_name ); + } + + public function sanitize_settings() { + parent::sanitize_settings(); + if ( is_array( $this->inputs ) ) { + foreach ( $this->inputs as &$input ) { + if ( isset ( $input['choices'] ) && is_array( $input['choices'] ) ) { + $input['choices'] = $this->sanitize_settings_choices( $input['choices'] ); + } + } + } + } + + public function get_form_inline_script_on_page_render( $form ) { + $script = sprintf( ';new GFChainedSelects( %d, %d, %d, "%s" );', $form['id'], $this->id, $this->chainedSelectsHideInactive, $this->chainedSelectsAlignment ); + + return $script; + } + + public function get_next_input_id( $current_input_id ) { + $index = $this->get_input_index( $current_input_id ); + $next_index = $index + 1; + if ( $next_index % 10 == 0 ) { + $next_index ++; + } + $next_input_id = sprintf( '%d.%d', intval( $current_input_id ), $next_index ); + // make sure the next input ID actually exists + foreach ( $this->inputs as $input ) { + if ( $input['id'] == $next_input_id ) { + return $next_input_id; + } + } + + return false; + } + + public function get_input_index( $input_id ) { + $id_bits = explode( '.', $input_id ); + + return (int) array_pop( $id_bits ); + } + + public function get_previous_input_value( $current_input_id, $full_chain_value ) { + + $input_id_bits = explode( '.', $current_input_id ); + + list( $field_id, $input_index ) = array_pad( $input_id_bits, 2, null ); + + $prev_input_id = sprintf( '%s.%s', $field_id, $input_index - 1 ); + $prev_input_value = rgar( $full_chain_value, $prev_input_id ); + + return $prev_input_value; + } + + // # FIELD FILTER UI HELPERS --------------------------------------------------------------------------------------- + + /** + * Returns the sub-filters for the current field. + * + * @since + * + * @return array + */ + public function get_filter_sub_filters() { + $sub_filters = array(); + $inputs = $this->inputs; + + foreach ( $inputs as $input ) { + $sub_filters[] = array( + 'key' => rgar( $input, 'id' ), + 'text' => rgar( $input, 'label' ), + 'preventMultiple' => false, + 'operators' => $this->get_filter_operators(), + ); + } + + return $sub_filters; + } + + /** + * Returns the filter operators for the current field. + * + * @since + * + * @return array + */ + public function get_filter_operators() { + $operators = parent::get_filter_operators(); + $operators[] = 'contains'; + + return $operators; + } + +} + +GF_Fields::register( new GF_Chained_Field_Select() ); diff --git a/js/admin-form-editor.js b/js/admin-form-editor.js new file mode 100644 index 0000000..8d750c3 --- /dev/null +++ b/js/admin-form-editor.js @@ -0,0 +1,419 @@ +/** + * GF Chained Selects Admin + */ +( function( $ ) { + + window.GFCSAdmin = { + + getDefaultInputs: function( field ) { + var inputs = $.extend( true, [], gformChainedSelectData.defaultInputs ); + for( var i = 0; i < inputs.length; i++ ) { + inputs[ i ].id = field.id + "." + ( i + 1 ); + } + return inputs; + }, + + updateAlignment: function( alignment ) { + + var field = GetSelectedField(); + field.chainedSelectsAlignment = alignment; + + $( '#field_' + field.id ).find( '.ginput_container' ).removeClass( 'horizontal vertical' ).addClass( alignment ); + + } + + }; + + var multipartParams = { + gform_unique_id: generateUniqueID(), + field_id: null, + form_id: null + }; + + multipartParams[ `_gform_file_upload_nonce_${form.id}` ] = window.gform_chainedselects_file_upload_nonce; + + var uploader = new plupload.Uploader( { + runtimes: 'html5,flash,silverlight,html4', + browse_button: document.getElementById( 'pickfiles' ), + container: document.getElementById( 'gfcs-container' ), + progress: document.getElementById ( 'gfcs-progress' ), + drop_element: document.getElementById ( 'gfcs-drop' ), + dragdrop: true, + url: gformChainedSelectData.fileUploadUrl, + filters: { + max_file_size: ( parseInt( gformChainedSelectData.maxFileSize ) / 1000000 ) + 'mb', + mime_types: [ + { title: '', extensions: 'csv' } + ] + }, + flash_swf_url: '/plupload/js/Moxie.swf', + silverlight_xap_url: '/plupload/js/Moxie.xap', + multipart_params: multipartParams, + init: { + PostInit: function ( up ) { + + $( document ).bind( 'gform_load_field_settings', function( event, field, form ) { + + if( ! isCorrectField( up ) ) { + abort( up ); + } + + if( field.type != 'chainedselect' ) { + return; + } else if( field.gfcsFilterEnabled && ! field.gfcsFile ) { + var $progress = $( up.settings.progress ); + $progress.html( getFilteredFileErrorMarkup() ); + toggleDropElement( up, false ); + return; + } + + up.field = $.extend( {}, field ); + + // reset's markup + document.getElementById( 'gfcs-progress' ).innerHTML = ''; + + if( field['gfcsFile'] ) { + updateUploadedFilePreview( up, field['gfcsFile'] ); + toggleDropElement( up, false ); + } else { + reset( up ); + } + + } ); + + $( up.settings.drop_element ).on( 'dragover', function() { + $( this ).addClass( 'gf-dragging' ); + } ).on( 'dragleave', function() { + $( this ).removeClass( 'gf-dragging' ); + } ); + + // fixes drag and drop in IE10 + $( up.settings.drop_element ).on( { + "dragenter": ignoreDrag, + "dragover": ignoreDrag + } ); + + $( up.settings.progress ).on( 'click', 'span.gfcs-remove', function() { + if( ! up.field['gfcsFilterEnabled'] ) { + reset( up ); + } + } ); + + gform.addFilter( 'gform_duplicate_field_chainedselect', function( field ) { + if( field.gfcsFilterEnabled ) { + field.gfcsFile = null; + field.gfcsFilterEnabled = false; + field.choices = getDefaultChoices(); + field.inputs = getDefaultInputs( field ); + } + return field; + } ); + + }, + FilesAdded: function ( up, files ) { + + toggleDropElement( up, false ); + + var max = 1, + totalCount = up.files.length, + isMaxExceeded = totalCount > max; + + files = up.files; + + if( isMaxExceeded ) { + var lastIndex = files.length - 1; + $.each( files, function ( i, file ) { + if( i < lastIndex ) { + up.removeFile( file ); + } + } ); + } + + $.each( files, function ( i, file ) { + + if( ( file.status == plupload.FAILED ) ) { + up.removeFile( file ); + return; + } + + updateUploadedFilePreview( up, file ); + + $( '#' + file.id ).attr( 'class', getStatusClass( file ) ); + + var multipartParams = up.getOption( 'multipart_params' ); + multipartParams.field_id = field.id; + multipartParams.form_id = form.id; + multipartParams.original_filename = file.name; + up.setOption( 'multipart_params', multipartParams ); + + } ); + + // Reposition Flash + up.refresh(); + + up.start(); + + }, + UploadProgress: function ( up, file ) { + + if( ! isCorrectField( up ) ) { + abort( up ); + return; + } + + document.getElementById( file.id ).getElementsByTagName( 'b' )[0].innerHTML = '' + file.percent + '%'; + + $( '#' + file.id ).attr( 'class', getStatusClass( file ) ); + + }, + Error: function ( up, err ) { + reset( up ); + var message = err.message; + if( err.code == -601 ) { + message = gformChainedSelectData.strings.errorFileType; + } else if( err.code == -600 ) { + message = gformChainedSelectData.strings.errorFileSize; + } + displayError( up, gformChainedSelectData.strings.errorUploadingFile + '
    Error: ' + err.code + ', Message: ' + message ); + }, + FileUploaded: function ( up, file, result ) { + + var response; + + try { + response = $.secureEvalJSON( result.response ); + } catch( e ) { + response = { status: 'error', error: { message: false } }; + } + if( response.status == 'error' ) { + reset( up ); + displayError( up, gformChainedSelectData.strings.errorProcessingFile + '
    Error: ' + response.error.code + ', Message: ' + response.error.message ); + return; + } + + if( file.percent == 100 && response.status && response.status == 'ok' ) { + + var field = GetSelectedField(); + + field.choices = response.data.choices; + field.inputs = response.data.inputs; + field.gfcsFile = { + name: file.name, + type: file.type, + size: file.size, + dateUploaded: Math.round( ( new Date() ).getTime() / 1000 ), + isFromFilter: false + }; + + updateFieldPreview( field ); + + $( '#' + file.id ).attr( 'class', getStatusClass( file ) ); + + } + + } + } + } ); + + function generateUniqueID() { + return 'xxxxxxxx'.replace( /[xy]/g, function ( c ) { + var r = Math.random() * 16 | 0, v = c == 'x' ? r : r & 0x3 | 0x8; + return v.toString( 16 ); + } ); + } + + function ignoreDrag( e ) { + e.preventDefault(); + } + + function toggleDropElement( up, isEnabled ) { + + var $drop = $( up.settings.drop_element ), + $sample = $( '#gfcs-sample' ); + + $drop.removeClass( 'gf-dragging' ); + + if( isEnabled ) { + $sample.show(); + $drop.show(); + } else { + $sample.hide(); + $drop.hide(); + } + + } + + function getFileMarkup( file ) { + + var size = plupload.formatSize( file.size ).toUpperCase(), + css = getStatusClass( file ), + dateUploaded = file.dateUploaded ? file.dateUploaded : Math.round( Date.now() / 1000 ), + removeButton = file.isFromFilter ? '' : '', + sourceMessage = file.isFromFilter ? getFilteredFileMarkup( file ) : ''; + + return ` +
    + ${file.name} ${size} | ${timeAgo( dateUploaded )} + + ${removeButton} + +
    + ${sourceMessage}`; + } + + function getFilteredFileMarkup() { + return `
    ${gformChainedSelectData.strings.importedFilterFile}
    `; + } + + function getFilteredFileErrorMarkup() { + return `
    ${gformChainedSelectData.strings.errorImportingFilterFile}
    `; + } + + function updateUploadedFilePreview( up, file ) { + var $progress = $( up.settings.progress ); + if( file ) { + $progress.html( getFileMarkup( file ) ); + } else { + $progress.html( '' ); + } + } + + function updateFieldPreview( field ) { + + var $inputContainer = $( '#field_' + field.id + ' .ginput_container' ), + markup = ''; + + for( var i = 0; i < field.inputs.length; i++ ) { + var options = ''; + markup += '' + "\n"; + } + + $inputContainer.html( markup ); + + } + + function isCorrectField( up ) { + var field = GetSelectedField(); + return field && ( typeof up.field == 'undefined' || up.field.id == field.id ); + } + + function abort( up ) { + up.stop(); + up.refresh(); + up.start(); + } + + function reset( up ) { + + up.files.splice( 0, up.files.length ); + up.refresh(); + up.start(); + + var field = GetFieldById( up.field.id ); + field.choices = gformChainedSelectData.defaultChoices; + field.inputs = getDefaultInputs( field ); + field.gfcsFile = null; + + updateFieldPreview( field ); + updateUploadedFilePreview( up, false ); + toggleDropElement( up, true ); + + } + + function getDefaultChoices() { + return gformChainedSelectData.defaultChoices; + } + + function getDefaultInputs( field ) { + var inputs = $.extend( true, [], gformChainedSelectData.defaultInputs ); + for( var i = 0; i < inputs.length; i++ ) { + inputs[ i ].id = field.id + "." + ( i + 1 ); + } + return inputs; + } + + function getStatusClass( file ) { + return `gfcs-status-${getStatus( file )}`; + } + + function getStatus( file ) { + + var status = ''; + + if( file.status && file.status == plupload.UPLOADING ) { + if( file.percent == 100 ) { + status = 'processing'; + } else { + status = 'uploading'; + } + } else { + status = 'complete'; + } + + return status; + } + + function displayError( up, message ) { + + // clear existing timeout and error (if needed) + delete window[ 'gfcsErrorTimeout' ]; + $( '#gfcs-error' ).remove(); + + var $error = $( `

    ${message}

    ` ); + + $( up.settings.progress ).html( $error ); + + window[ 'gfcsErrorTimeout' ] = setTimeout( function() { + $error.slideUp( function() { + $error.remove(); + } ); + }, 10000 ); + + } + + function timeAgo( timestamp ) { + + var diff = ( Date.now() / 1000 ) - timestamp, + formats = { + hours: { period: 60 * 60, label: { singular: 'hour', plural: 'hours' } }, + minutes: { period: 60, label: { singular: 'min', plural: 'mins' } }, + seconds: { period: 1, label: { singular: 'sec', plural: 'secs' } } + }; + + for ( var key in formats ) { + + if( ! formats.hasOwnProperty( key ) ) { + continue; + } + + var format = formats[ key ], + count = Math.round( diff / format.period ), + output = ''; + + if( key == 'hours' && count >= 24 ) { + break; + } else if( key == 'seconds' && count == 0 ) { + output = `${1} ${format.label.singular} ago`; + } else if( count > 0 ) { + var label = count > 1 ? format.label.plural : format.label.singular; + output = `${count} ${label} ago`; + break; + } + + } + + if( ! output ) { + + var date = new Date( timestamp * 1000 ), + dateBits = date.toDateString().split( ' ' ); + + output = `${dateBits[1]} ${dateBits[2]}, ${dateBits[3]}`; + + } + + return output; + } + + uploader.init(); + +} )( jQuery ); \ No newline at end of file diff --git a/js/admin-form-editor.min.js b/js/admin-form-editor.min.js new file mode 100644 index 0000000..6391015 --- /dev/null +++ b/js/admin-form-editor.min.js @@ -0,0 +1,8 @@ +!function(r){window.GFCSAdmin={getDefaultInputs:function(e){for(var t=r.extend(!0,[],gformChainedSelectData.defaultInputs),i=0;i',n=e.isFromFilter?`
    ${gformChainedSelectData.strings.importedFilterFile}
    `:"";return` +
    + ${e.name} ${t} | ${function(e){var t,i=Date.now()/1e3-e,s={hours:{period:3600,label:{singular:"hour",plural:"hours"}},minutes:{period:60,label:{singular:"min",plural:"mins"}},seconds:{period:1,label:{singular:"sec",plural:"secs"}}};for(t in s)if(s.hasOwnProperty(t)){var r=s[t],n=Math.round(i/r.period),a="";if("hours"==t&&24<=n)break;if("seconds"==t&&0==n)a=`1 ${r.label.singular} ago`;else if(0 + + ${r} + +
    + `+n}function a(e,t){e=r(e.settings.progress);t?e.html(i(t)):e.html("")}function l(e){for(var t=r("#field_"+e.id+" .ginput_container"),i="",s=0;s\n";t.html(i)}function o(e){var t=GetSelectedField();return t&&(void 0===e.field||e.field.id==t.id)}function d(e){e.stop(),e.refresh(),e.start()}function c(e){e.files.splice(0,e.files.length),e.refresh(),e.start();var t=GetFieldById(e.field.id);t.choices=gformChainedSelectData.defaultChoices,t.inputs=f(t),t.gfcsFile=null,l(t),a(e,!1),n(e,!0)}function f(e){for(var t=r.extend(!0,[],gformChainedSelectData.defaultInputs),i=0;i

    ${t}

    `);r(e.settings.progress).html(i),window.gfcsErrorTimeout=setTimeout(function(){i.slideUp(function(){i.remove()})},1e4)}e["_gform_file_upload_nonce_"+form.id]=window.gform_chainedselects_file_upload_nonce,new plupload.Uploader({runtimes:"html5,flash,silverlight,html4",browse_button:document.getElementById("pickfiles"),container:document.getElementById("gfcs-container"),progress:document.getElementById("gfcs-progress"),drop_element:document.getElementById("gfcs-drop"),dragdrop:!0,url:gformChainedSelectData.fileUploadUrl,filters:{max_file_size:parseInt(gformChainedSelectData.maxFileSize)/1e6+"mb",mime_types:[{title:"",extensions:"csv"}]},flash_swf_url:"/plupload/js/Moxie.swf",silverlight_xap_url:"/plupload/js/Moxie.xap",multipart_params:e,init:{PostInit:function(s){r(document).bind("gform_load_field_settings",function(e,t,i){o(s)||d(s),"chainedselect"==t.type&&(t.gfcsFilterEnabled&&!t.gfcsFile?(r(s.settings.progress).html(`
    ${gformChainedSelectData.strings.errorImportingFilterFile}
    `),n(s,!1)):(s.field=r.extend({},t),document.getElementById("gfcs-progress").innerHTML="",t.gfcsFile?(a(s,t.gfcsFile),n(s,!1)):c(s)))}),r(s.settings.drop_element).on("dragover",function(){r(this).addClass("gf-dragging")}).on("dragleave",function(){r(this).removeClass("gf-dragging")}),r(s.settings.drop_element).on({dragenter:t,dragover:t}),r(s.settings.progress).on("click","span.gfcs-remove",function(){s.field.gfcsFilterEnabled||c(s)}),gform.addFilter("gform_duplicate_field_chainedselect",function(e){return e.gfcsFilterEnabled&&(e.gfcsFile=null,e.gfcsFilterEnabled=!1,e.choices=gformChainedSelectData.defaultChoices,e.inputs=f(e)),e})},FilesAdded:function(s,e){n(s,!1);var i,t=s.files.length;e=s.files,1"+t.percent+"%",r("#"+t.id).attr("class",g(t))):d(e)},Error:function(e,t){c(e);var i=t.message;-601==t.code?i=gformChainedSelectData.strings.errorFileType:-600==t.code&&(i=gformChainedSelectData.strings.errorFileSize),u(e,gformChainedSelectData.strings.errorUploadingFile+"
    Error: "+t.code+", Message: "+i)},FileUploaded:function(e,t,i){var s;try{s=r.secureEvalJSON(i.response)}catch(e){s={status:"error",error:{message:!1}}}"error"==s.status?(c(e),u(e,gformChainedSelectData.strings.errorProcessingFile+"
    Error: "+s.error.code+", Message: "+s.error.message)):100==t.percent&&s.status&&"ok"==s.status&&((i=GetSelectedField()).choices=s.data.choices,i.inputs=s.data.inputs,i.gfcsFile={name:t.name,type:t.type,size:t.size,dateUploaded:Math.round((new Date).getTime()/1e3),isFromFilter:!1},l(i),r("#"+t.id).attr("class",g(t)))}}}).init()}(jQuery); \ No newline at end of file diff --git a/js/admin.js b/js/admin.js new file mode 100644 index 0000000..f8c092f --- /dev/null +++ b/js/admin.js @@ -0,0 +1,110 @@ +/** + * Conditional Logic Functionality + */ +( function( $ ) { + + /* + * The input-specific fields will be added automatically by GF. Let's add our top level field id as an option as well. + */ + gform.addFilter( 'gform_conditional_logic_fields', function( options, form ) { + + $.each( form.fields, function( i, field ) { + + if( GetInputType( field ) == 'chainedselect' ) { + + // find the first option from our field + var optionIndex = false; + for( var j = 0; j < options.length; j++ ) { + if( parseInt( options[j].value ) == field.id ) { + optionIndex = j; + break; + } + } + + options.splice( optionIndex, 0, { + label: GetLabel( field ), + value: field.id + } ); + + } + + } ); + + return options; + } ); + + gform.addFilter( 'gform_conditional_logic_values_input', function( markup, objectType, ruleIndex, selectedFieldId, selectedValue ) { + + var field = GetFieldById( selectedFieldId ), + isInputSpecific = parseInt( selectedFieldId ) != selectedFieldId, + value = typeof selectedValue == 'undefined' ? '' : selectedValue; + + if( ! field || GetInputType( field ) != 'chainedselect' ) { + return markup; + } + + if( ! isInputSpecific ) { + var placeholder = []; + $.each( field.inputs, function( i, input ) { + placeholder.push( GetLabel( field, input.id, true ) ); + } ); + markup = ''; + } else { + markup = ''; + var emptyOption = ''; + $.each( getAllChoicesByInputId( selectedFieldId, field ), function( i, choice ) { + var selectedMarkup = choice.value == value ? 'selected="selected"' : ''; + markup += ''; + } ); + markup = ''; + } + return markup; + } ); + + function getAllChoicesByInputId( inputId, obj, depth, inputChoices ) { + + var targetDepth = parseInt( String( inputId ).split( '.' )[1] ); // converts "4.3" to 3 + + if( typeof depth == 'undefined' ) { + depth = 1; + } + + if( typeof inputChoices == 'undefined' ) { + inputChoices = []; + } + + if( typeof obj.choices == 'undefined' || ! obj.choices ) { + return inputChoices; + } + + if( depth == targetDepth ) { + inputChoices = inputChoices.concat( obj.choices ); + } else { + $.each( obj.choices, function( i, choice ) { + inputChoices = getAllChoicesByInputId( inputId, choice, depth + 1, inputChoices ); + } ); + } + + if( depth == 1 ) { + var values = []; + for( var i = inputChoices.length - 1; i >= 0; i-- ) { + if( $.inArray( inputChoices[i].value, values ) == -1 ) { + values.push( inputChoices[i].value ); + } else { + inputChoices.splice( i, 1 ); + } + } + } + + if( depth == 1 ) { + inputChoices.sort( function( a, b ) { + var x = a.value.toString().toLowerCase(), + y = b.value.toString().toLowerCase(); + return x < y ? -1 : x > y ? 1 : 0; + } ); + } + + return inputChoices; + } + +} )( jQuery ); diff --git a/js/admin.min.js b/js/admin.min.js new file mode 100644 index 0000000..237be97 --- /dev/null +++ b/js/admin.min.js @@ -0,0 +1 @@ +!function(c){gform.addFilter("gform_conditional_logic_fields",function(i,e){return c.each(e.fields,function(e,t){if("chainedselect"==GetInputType(t)){for(var o=!1,l=0;l"+t.text+""}),'"):(u=[],c.each(n.inputs,function(e,t){u.push(GetLabel(n,t.id,!0))}),''):l})}(jQuery); \ No newline at end of file diff --git a/js/frontend.js b/js/frontend.js new file mode 100644 index 0000000..f9c6951 --- /dev/null +++ b/js/frontend.js @@ -0,0 +1,318 @@ +/** + * GF Chained Selects Frontend + */ + +( function( $ ) { + + window.GFChainedSelects = function( formId, fieldId, hideInactive, alignment ) { + + var self = this; + + self.formId = formId; + self.fieldId = fieldId; + self.hideInactive = hideInactive; + self.alignment = alignment; + + var $field = $( '#field_' + self.formId + '_' + self.fieldId ); + + self.$selects = $field.find( 'select' ); + self.$complete = $field.find( '.gf_chain_complete' ); + + self.isDoingConditionalLogic = false; + + self.init = function() { + + gform.addAction( 'gform_input_change', function( elem, formId, fieldId ) { + if( self.$selects.index( elem ) != - 1 ) { + var inputId = $( elem ).attr( 'name' ).split( '_' )[1]; // converts "input_4.1" to "4.1" + self.populateNextChoices( inputId, elem.value, $( elem ) ); + } + }, 9 ); + + self.$selects.filter( function() { + var $select = $( this ); + return $select.hasClass( 'gf_no_options' ) || $select.find( 'option' ).length <= 1; + } ).toggleSelect( true, self.hideInactive ); + //.prop( 'disabled', true ).hide(); + + /*var $lastSelect = self.$selects.last(); + self.toggleCompleted( $lastSelect.hasClass( 'gf_no_options' ) || $lastSelect.val() );*/ + + gform.addFilter( 'gform_is_value_match', function( isMatch, formId, rule ) { + return self.isValueMatch( isMatch, formId, rule ); + } ); + + }; + + self.populateNextChoices = function( inputId, selectedValue, $select ) { + + var nextInputId = self.getNextInputId( inputId ), + $nextSelect = self.$selects.filter( '[name="input_' + nextInputId + '"]' ); + + // if there is no $nextSelect, we're at the end of our chain + if( $nextSelect.length <= 0 ) { + self.resetSelects( $select, true ); + self.resizeSelects(); + return; + } else { + self.resetSelects( $select ); + } + + if( ! selectedValue ) { + return; + } + + if( self.hideInactive ) { + + var $currentSelect = self.$selects.filter( '[name="input_' + inputId + '" ]' ), + $spinner = new gfAjaxSpinner( $currentSelect, gformChainedSelectData.spinner, 'display:inline-block;vertical-align:middle;margin:-1px 0 0 6px;', inputId ); + + } else { + + var $loadingOption = $( '' ), + dotCount = 2, + loadingInterval = setInterval( function() { + $loadingOption.text( gformChainedSelectData.strings.loading + ( new Array( dotCount ).join( '.' ) ) ); + dotCount = dotCount > 3 ? 0 : dotCount + 1; + }, 250 ); + + $loadingOption.prependTo( $nextSelect ).prop( 'selected', true ); + $nextSelect.css( { minWidth: $nextSelect.width() } ); + $loadingOption.text( gformChainedSelectData.strings.loading + '.' ); + + } + + $.post( gformChainedSelectData.ajaxUrl, { + action: 'gform_get_next_chained_select_choices', + input_id: inputId, + form_id: self.formId, + field_id: self.fieldId, + value: self.getChainedSelectsValue(), + nonce: gformChainedSelectData.nonce + }, function( response ) { + + if( self.hideInactive ) { + + $spinner.destroy(); + + } else { + + clearInterval( loadingInterval ); + $loadingOption.remove(); + + } + + if( ! response ) { + return; + } + + var choices = $.parseJSON( response ), + optionsMarkup = ''; + + $nextSelect.find( 'option:not(:first)' ).remove(); + + if( choices.length <= 0 ) { + + self.resetSelects( $select, true ); + + } else { + + var hasSelectedChoice = false; + + $.each( choices, function( i, choice ) { + var selected = choice.isSelected ? 'selected="selected"' : ''; + + if ( selected ) { + hasSelectedChoice = true; + } + + optionsMarkup += ''; + } ); + + $nextSelect.show().append( optionsMarkup ); + + // the placeholder will be selected by default, rather than removing it and re-adding, just force the noOptions option to be selected + if( choices[0].noOptions ) { + + var $noOption = $nextSelect.find( 'option:last-child' ).clone(), + $nextSelects = $nextSelect.parents( 'span' ).nextAll().find( 'select' ); + + $nextSelects.append( $noOption ); + + $nextSelects.add( $nextSelect ) + .addClass( 'gf_no_options' ) + .find( 'option:last-child' ).prop( 'selected', true ); + + //self.toggleCompleted( true ); + + } else { + $nextSelect + .removeClass( 'gf_no_options' ) + //.prop( 'disabled', false ).show(); + .toggleSelect( false, self ); + + if ( hasSelectedChoice ) { + $nextSelect.change(); + } + } + + } + + self.resizeSelects(); + + } ); + + }; + + self.getChainedSelectsValue = function() { + + var value = {}; + + self.$selects.each( function() { + var inputId = $( this ).attr( 'name' ).split( '_' )[1]; // converts "input_4.1" to "4.1" + value[ inputId ] = $( this ).val(); + } ); + + return value; + }; + + self.getNextInputId = function( currentInputId ) { + + var index = parseInt( currentInputId.split( '.' )[1] ), + nextIndex = index + 1; + + if( nextIndex % 10 == 0 ) { + nextIndex++; + } + + return parseInt( currentInputId ) + '.' + ( nextIndex ); + }; + + self.resetSelects = function( $currentSelect, isComplete ) { + + var currentIndex = self.$selects.index( $currentSelect ), + $nextSelects = self.$selects.filter( ':gt(' + currentIndex + ')' ); + + $nextSelects + .toggleSelect( true, self.hideInactive ) + .find( 'option:not(:first)' ) + .remove() + .val( '' ) + .change(); + + }; + + self.resizeSelects = function() { + + if( self.alignment != 'vertical' ) { + return; + } + + // reset width so it will be determined by its contents + self.$selects.width( 'auto' ); + + var width = 0; + + self.$selects.each( function() { + if( $( this ).width() > width ) { + width = $( this ).width(); + } + } ); + + self.$selects.width( width + 'px' ); + + }; + + self.toggleCompleted = function( isComplete ) { + if( isComplete ) { + self.$complete.fadeIn(); + } else { + self.$complete.fadeOut(); + } + }; + + self.isValueMatch = function( isMatch, formId, rule ) { + + if( formId != self.formId || rule.fieldId != self.fieldId || self.isDoingConditionalLogic ) { + return isMatch; + } + + self.isDoingConditionalLogic = true; + + rule = $.extend( {}, rule ); + + var valueObj = self.getChainedSelectsValue(), + fieldValue = Object.keys( valueObj ).map( function( key ) { return valueObj[ key ]; } ), + ruleValue = rule.value.split( '/' ); + + for( var i = 0; i < ruleValue.length; i++ ) { + if( ruleValue[i] == '*' ) { + ruleValue[i] = fieldValue[i]; + } + } + + ruleValue = ruleValue.join( '/' ); + fieldValue = fieldValue.join( '/' ); + + isMatch = gf_matches_operation( ruleValue, fieldValue, rule.operator ); + + self.isDoingConditionalLogic = false; + + return isMatch; + }; + + $.fn.toggleSelect = function( disabled, hideInactive ) { + this.prop( 'disabled', disabled ); + if( typeof hideInactive != 'undefined' && hideInactive ) { + if( disabled ) { + this.hide(); + } else { + this.show(); + } + } + return this; + }; + + self.init(); + + }; + + function gfAjaxSpinner( elem, imageSrc, inlineStyles, inputId = 0 ) { + + var imageSrc = typeof imageSrc == 'undefined' ? '/images/ajax-loader.gif': imageSrc, + inlineStyles = typeof inlineStyles != 'undefined' ? inlineStyles : ''; + + this.elem = elem; + this.formId = elem.parents( 'form' ).data( 'formid' ); + this.image = ''; + + this.init = function() { + + if ( 'function' !== typeof gformInitializeSpinner || !this.formUsesFramework( this.formId ) ) { + this.spinner = jQuery( this.image ); + jQuery( this.elem ).after( this.spinner ); + return this; + } + + var $spinnerTarget = this.elem.closest( 'span' ); + gformInitializeSpinner( this.formId, $spinnerTarget, 'gform-chainedselect-spinner-' + inputId ); + }; + + this.destroy = function() { + + if ( 'function' !== typeof gformRemoveSpinner || ! this.formUsesFramework( this.formId ) ) { + jQuery( this.spinner ).remove(); + return; + } + + gformRemoveSpinner( 'gform-chainedselect-spinner-' + inputId ); + }; + + this.formUsesFramework = function( formId ) { + return jQuery( '#gform_wrapper_' + formId ).hasClass( 'gform-theme--framework' ); + } + + return this.init(); + } + +} )( jQuery ); diff --git a/js/frontend.min.js b/js/frontend.min.js new file mode 100644 index 0000000..a140313 --- /dev/null +++ b/js/frontend.min.js @@ -0,0 +1 @@ +!function(d){function f(e,t,i,n=0){t=void 0===t?"/images/ajax-loader.gif":t,i=void 0!==i?i:"";return this.elem=e,this.formId=e.parents("form").data("formid"),this.image='',this.init=function(){if("function"!=typeof gformInitializeSpinner||!this.formUsesFramework(this.formId))return this.spinner=jQuery(this.image),jQuery(this.elem).after(this.spinner),this;var e=this.elem.closest("span");gformInitializeSpinner(this.formId,e,"gform-chainedselect-spinner-"+n)},this.destroy=function(){"function"==typeof gformRemoveSpinner&&this.formUsesFramework(this.formId)?gformRemoveSpinner("gform-chainedselect-spinner-"+n):jQuery(this.spinner).remove()},this.formUsesFramework=function(e){return jQuery("#gform_wrapper_"+e).hasClass("gform-theme--framework")},this.init()}window.GFChainedSelects=function(e,t,i,n){var c=this,e=(c.formId=e,c.fieldId=t,c.hideInactive=i,c.alignment=n,d("#field_"+c.formId+"_"+c.fieldId));c.$selects=e.find("select"),c.$complete=e.find(".gf_chain_complete"),c.isDoingConditionalLogic=!1,c.init=function(){gform.addAction("gform_input_change",function(e,t,i){var n;-1!=c.$selects.index(e)&&(n=d(e).attr("name").split("_")[1],c.populateNextChoices(n,e.value,d(e)))},9),c.$selects.filter(function(){var e=d(this);return e.hasClass("gf_no_options")||e.find("option").length<=1}).toggleSelect(!0,c.hideInactive),gform.addFilter("gform_is_value_match",function(e,t,i){return c.isValueMatch(e,t,i)})},c.populateNextChoices=function(e,t,i){var s,r,n,a,o=c.getNextInputId(e),l=c.$selects.filter('[name="input_'+o+'"]');l.length<=0?(c.resetSelects(i,!0),c.resizeSelects()):(c.resetSelects(i),t&&(c.hideInactive?s=new f(c.$selects.filter('[name="input_'+e+'" ]'),gformChainedSelectData.spinner,"display:inline-block;vertical-align:middle;margin:-1px 0 0 6px;",e):(r=d('"),n=2,a=setInterval(function(){r.text(gformChainedSelectData.strings.loading+new Array(n).join(".")),n=3"+t.text+""}),l.show().append(n),e[0].noOptions?(e=l.find("option:last-child").clone(),(t=l.parents("span").nextAll().find("select")).append(e),t.add(l).addClass("gf_no_options").find("option:last-child").prop("selected",!0)):(l.removeClass("gf_no_options").toggleSelect(!1,c),o&&l.change())),c.resizeSelects())})))},c.getChainedSelectsValue=function(){var t={};return c.$selects.each(function(){var e=d(this).attr("name").split("_")[1];t[e]=d(this).val()}),t},c.getNextInputId=function(e){var t=parseInt(e.split(".")[1])+1;return t%10==0&&t++,parseInt(e)+"."+t},c.resetSelects=function(e,t){e=c.$selects.index(e);c.$selects.filter(":gt("+e+")").toggleSelect(!0,c.hideInactive).find("option:not(:first)").remove().val("").change()},c.resizeSelects=function(){var e;"vertical"==c.alignment&&(c.$selects.width("auto"),e=0,c.$selects.each(function(){d(this).width()>e&&(e=d(this).width())}),c.$selects.width(e+"px"))},c.toggleCompleted=function(e){e?c.$complete.fadeIn():c.$complete.fadeOut()},c.isValueMatch=function(e,t,i){if(t==c.formId&&i.fieldId==c.fieldId&&!c.isDoingConditionalLogic){c.isDoingConditionalLogic=!0,i=d.extend({},i);for(var n=c.getChainedSelectsValue(),o=Object.keys(n).map(function(e){return n[e]}),s=i.value.split("/"),r=0;r\n" +"Language-Team: Gravity Forms \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"POT-Creation-Date: 2023-11-29T18:17:47+00:00\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"X-Generator: WP-CLI 2.8.1\n" +"X-Domain: gravityformschainedselects\n" + +#. Plugin Name of the plugin +msgid "Gravity Forms Chained Selects Add-On" +msgstr "" + +#. Plugin URI of the plugin +#. Author URI of the plugin +msgid "https://gravityforms.com" +msgstr "" + +#. Description of the plugin +msgid "Adds the powerful Chained Selects field type, allowing you to chain multiple Drop Downs together (e.g. Make, Model, Year)." +msgstr "" + +#. Author of the plugin +msgid "Gravity Forms" +msgstr "" + +#: class-gf-chainedselects.php:188 +msgid "There was an error processing this file." +msgstr "" + +#: class-gf-chainedselects.php:189 +msgid "There was an error uploading this file." +msgstr "" + +#: class-gf-chainedselects.php:190 +msgid "Only CSV files are allowed." +msgstr "" + +#: class-gf-chainedselects.php:191 +msgid "This file is too big. Max file size is %dMB." +msgstr "" + +#: class-gf-chainedselects.php:192 +msgid "This file is imported via %sa filter%s and cannot be modified here." +msgstr "" + +#: class-gf-chainedselects.php:193 +msgid "There was an error importing the file via %sthe filter%s." +msgstr "" + +#: class-gf-chainedselects.php:202 +msgid "Loading" +msgstr "" + +#: class-gf-chainedselects.php:203 +#: includes/class-gf-field-chainedselect.php:776 +msgid "No options" +msgstr "" + +#: class-gf-chainedselects.php:224 +#: class-gf-chainedselects.php:225 +msgid "Parent 1" +msgstr "" + +#: class-gf-chainedselects.php:229 +#: class-gf-chainedselects.php:230 +msgid "Child 1" +msgstr "" + +#: class-gf-chainedselects.php:234 +#: class-gf-chainedselects.php:235 +msgid "Child 2" +msgstr "" + +#: class-gf-chainedselects.php:239 +#: class-gf-chainedselects.php:240 +msgid "Child 3" +msgstr "" + +#: class-gf-chainedselects.php:246 +#: class-gf-chainedselects.php:247 +msgid "Parent 2" +msgstr "" + +#: class-gf-chainedselects.php:251 +#: class-gf-chainedselects.php:252 +msgid "Child 4" +msgstr "" + +#: class-gf-chainedselects.php:256 +#: class-gf-chainedselects.php:257 +msgid "Child 5" +msgstr "" + +#: class-gf-chainedselects.php:261 +#: class-gf-chainedselects.php:262 +msgid "Child 6" +msgstr "" + +#: class-gf-chainedselects.php:268 +#: class-gf-chainedselects.php:269 +msgid "Parent 3" +msgstr "" + +#: class-gf-chainedselects.php:273 +#: class-gf-chainedselects.php:274 +msgid "Child 7" +msgstr "" + +#: class-gf-chainedselects.php:278 +#: class-gf-chainedselects.php:279 +msgid "Child 8" +msgstr "" + +#: class-gf-chainedselects.php:283 +#: class-gf-chainedselects.php:284 +msgid "Child 9" +msgstr "" + +#: class-gf-chainedselects.php:295 +msgid "Parents" +msgstr "" + +#: class-gf-chainedselects.php:299 +msgid "Children" +msgstr "" + +#: includes/class-gf-field-chainedselect.php:38 +msgid "Allows populating drop downs dynamically and chaining multiple drop downs together." +msgstr "" + +#: includes/class-gf-field-chainedselect.php:58 +msgid "Choices" +msgstr "" + +#: includes/class-gf-field-chainedselect.php:58 +msgid "Upload a .csv file to import your choices." +msgstr "" + +#: includes/class-gf-field-chainedselect.php:59 +msgid "Bulk Add Disabled" +msgstr "" + +#: includes/class-gf-field-chainedselect.php:59 +msgid "Bulk Add is only available for the last Drop Down in a chain. It is best to use Bulk Add for each Drop Down as you build your chain." +msgstr "" + +#: includes/class-gf-field-chainedselect.php:129 +msgid "One of your columns has exceeded the limit for unique values." +msgstr "" + +#: includes/class-gf-field-chainedselect.php:394 +msgid "File could not be loaded." +msgstr "" + +#: includes/class-gf-field-chainedselect.php:396 +msgid "File is not a CSV file." +msgstr "" + +#: includes/class-gf-field-chainedselect.php:398 +msgid "File is empty." +msgstr "" + +#: includes/class-gf-field-chainedselect.php:413 +msgid "File is too large." +msgstr "" + +#: includes/class-gf-field-chainedselect.php:478 +msgid "Import Choices" +msgstr "" + +#: includes/class-gf-field-chainedselect.php:483 +msgid "Your browser does not have Flash, Silverlight or HTML5 support." +msgstr "" + +#: includes/class-gf-field-chainedselect.php:485 +msgid "Drop your file here or " +msgstr "" + +#: includes/class-gf-field-chainedselect.php:486 +msgid "Select a file" +msgstr "" + +#: includes/class-gf-field-chainedselect.php:489 +msgid "Download a sample file: %ssample.csv%s" +msgstr "" + +#: includes/class-gf-field-chainedselect.php:505 +msgid "Drop Down Alignment" +msgstr "" + +#: includes/class-gf-field-chainedselect.php:508 +msgid "Horizontally (in a row)" +msgstr "" + +#: includes/class-gf-field-chainedselect.php:509 +msgid "Vertically (in a column)" +msgstr "" + +#: includes/class-gf-field-chainedselect.php:514 +msgid "Drop Down Display" +msgstr "" + +#: includes/class-gf-field-chainedselect.php:518 +msgid "Hide Inactive Drop Downs" +msgstr "" + +#: includes/class-gf-field-chainedselect.php:548 +msgid "Chained Selects" +msgstr "" + +#: includes/class-gf-field-chainedselect.php:616 +msgid "This field is required. Please select a value for each option." +msgstr ""