├── screenshot-1.png ├── screenshot-2.png ├── screenshot-3.png ├── cmb2-group-map.zip ├── package.json ├── composer.json ├── lib ├── assets │ ├── css │ │ └── cmb2-group-map.css │ └── js │ │ └── cmb2-group-map.js ├── base.php ├── ajax.php ├── set.php ├── get.php └── init.php ├── languages └── cmb2-group-map.pot ├── .gitignore ├── composer.lock ├── Gruntfile.js ├── README.md └── cmb2-group-map.php /screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CMB2/cmb2-group-map/HEAD/screenshot-1.png -------------------------------------------------------------------------------- /screenshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CMB2/cmb2-group-map/HEAD/screenshot-2.png -------------------------------------------------------------------------------- /screenshot-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CMB2/cmb2-group-map/HEAD/screenshot-3.png -------------------------------------------------------------------------------- /cmb2-group-map.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CMB2/cmb2-group-map/HEAD/cmb2-group-map.zip -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cmb2-group-map", 3 | "title": "CMB2 Group Map", 4 | "version": "0.1.0", 5 | "description": "CMB2 addon which allows you to use CMB2 group fields to manage custom post type entries.", 6 | "author": { 7 | "name": "zao", 8 | "url": "http://zao.is/" 9 | }, 10 | "license": "GPLv2", 11 | "devDependencies": { 12 | "grunt": "latest", 13 | "grunt-contrib-watch": "latest", 14 | "grunt-wp-i18n": "latest", 15 | "grunt-contrib-compress": "~1.3.0", 16 | "grunt-githooks": "~0.6.0", 17 | "load-grunt-tasks": "latest" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zao-web/cmb2-group-map", 3 | "description": "Allows using a custom post type for a CMB2 Group data store.", 4 | "license": "GPL-2.0+", 5 | "authors": [ 6 | { 7 | "name": "zao", 8 | "email": "jt@zao.is", 9 | "homepage": "http://zao.is/", 10 | "role": "Developer" 11 | } 12 | ], 13 | "keywords": ["wordpress", "plugin", "metabox","search","field","CMB2"], 14 | "homepage": "http://zao.is", 15 | "type": "wordpress-plugin", 16 | "require": { 17 | "php": ">5.2.4", 18 | "webdevstudios/cmb2-post-search-field": "dev-master" 19 | }, 20 | "suggest": { 21 | "composer/installers": "~1.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/assets/css/cmb2-group-map.css: -------------------------------------------------------------------------------- 1 | .cmb2-wrap .cmb2-mapping-select { 2 | padding: 0 20px; 3 | font-size: 1.5em; 4 | text-indent: -10px; 5 | margin-right: 12px; 6 | } 7 | .cmb2-group-map-id { 8 | display: none; 9 | } 10 | .cmb2-group-map-data.regular-text { 11 | width: auto; 12 | text-align: right; 13 | background: none; 14 | padding: 4px; 15 | outline: none; 16 | box-shadow: none; 17 | border: none; 18 | } 19 | .cmb2-group-map-id-wrap { 20 | margin-right: -4px; 21 | padding: 5px 6px 4px 8px; 22 | border: 1px solid #ddd; 23 | -webkit-box-shadow: inset 0 1px 2px rgba( 0, 0, 0, 0.07 ); 24 | box-shadow: inset 0 1px 2px rgba( 0, 0, 0, 0.07 ); 25 | background-color: #fff; 26 | color: #32373c; 27 | outline: none; 28 | border-right: none; 29 | border-radius: 3px; 30 | } 31 | -------------------------------------------------------------------------------- /languages/cmb2-group-map.pot: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2017 Zao 2 | # This file is distributed under the GPLv2. 3 | msgid "" 4 | msgstr "" 5 | "Project-Id-Version: CMB2 Group Map 0.1.0\n" 6 | "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/cmb2-group-map\n" 7 | "POT-Creation-Date: 2017-01-23 14:28:49+00:00\n" 8 | "MIME-Version: 1.0\n" 9 | "Content-Type: text/plain; charset=utf-8\n" 10 | "Content-Transfer-Encoding: 8bit\n" 11 | "PO-Revision-Date: 2017-MO-DA HO:MI+ZONE\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language-Team: LANGUAGE \n" 14 | "X-Generator: grunt-wp-i18n 0.5.4\n" 15 | 16 | #: vendor/webdevstudios/cmb2-post-search-field/lib/init.php:47 17 | msgid "An error has occurred. Please reload the page and try again." 18 | msgstr "" 19 | 20 | #: vendor/webdevstudios/cmb2-post-search-field/lib/init.php:48 21 | msgid "Find Posts or Pages" 22 | msgstr "" 23 | 24 | #. Plugin Name of the plugin/theme 25 | msgid "CMB2 Group Map" 26 | msgstr "" 27 | 28 | #. Plugin URI of the plugin/theme 29 | msgid "https://github.com/zao-web/cmb2-group-map" 30 | msgstr "" 31 | 32 | #. Description of the plugin/theme 33 | msgid "" 34 | "CMB2 addon which allows you to use CMB2 group fields to manage custom post " 35 | "type entries." 36 | msgstr "" 37 | 38 | #. Author of the plugin/theme 39 | msgid "Zao" 40 | msgstr "" 41 | 42 | #. Author URI of the plugin/theme 43 | msgid "http://zao.is" 44 | msgstr "" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/osx,node,grunt,composer,sass 2 | 3 | ### OSX ### 4 | .DS_Store 5 | .AppleDouble 6 | .LSOverride 7 | 8 | # Icon must end with two \r 9 | Icon 10 | 11 | 12 | # Thumbnails 13 | ._* 14 | 15 | # Files that might appear in the root of a volume 16 | .DocumentRevisions-V100 17 | .fseventsd 18 | .Spotlight-V100 19 | .TemporaryItems 20 | .Trashes 21 | .VolumeIcon.icns 22 | 23 | # Directories potentially created on remote AFP share 24 | .AppleDB 25 | .AppleDesktop 26 | Network Trash Folder 27 | Temporary Items 28 | .apdisk 29 | 30 | 31 | ### Node ### 32 | # Logs 33 | logs 34 | *.log 35 | npm-debug.log* 36 | 37 | # Runtime data 38 | pids 39 | *.pid 40 | *.seed 41 | 42 | # Directory for instrumented libs generated by jscoverage/JSCover 43 | lib-cov 44 | 45 | # Coverage directory used by tools like istanbul 46 | coverage 47 | 48 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 49 | .grunt 50 | 51 | # node-waf configuration 52 | .lock-wscript 53 | 54 | # Compiled binary addons (http://nodejs.org/api/addons.html) 55 | build/Release 56 | 57 | # Dependency directories 58 | node_modules 59 | jspm_packages 60 | 61 | # Optional npm cache directory 62 | .npm 63 | 64 | # Optional REPL history 65 | .node_repl_history 66 | 67 | 68 | ### grunt ### 69 | # Grunt usually compiles files inside this directory 70 | dist/ 71 | 72 | # Grunt usually preprocesses files such as coffeescript, compass... inside the .tmp directory 73 | .tmp/ 74 | 75 | 76 | ### Composer ### 77 | composer.phar 78 | /vendor/ 79 | 80 | # Commit your application's lock file http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file 81 | # You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file 82 | # composer.lock 83 | 84 | 85 | ### Sass ### 86 | .sass-cache/ 87 | *.css.map 88 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", 5 | "This file is @generated automatically" 6 | ], 7 | "hash": "855cd3775942376bd61cfb02bb97e928", 8 | "content-hash": "8ad819caa7f6464155b454ae3a135b2a", 9 | "packages": [ 10 | { 11 | "name": "webdevstudios/cmb2-post-search-field", 12 | "version": "dev-master", 13 | "source": { 14 | "type": "git", 15 | "url": "https://github.com/WebDevStudios/CMB2-Post-Search-field.git", 16 | "reference": "50413ef784a4d1cd23e10a54bb036ea473443324" 17 | }, 18 | "dist": { 19 | "type": "zip", 20 | "url": "https://api.github.com/repos/WebDevStudios/CMB2-Post-Search-field/zipball/50413ef784a4d1cd23e10a54bb036ea473443324", 21 | "reference": "50413ef784a4d1cd23e10a54bb036ea473443324", 22 | "shasum": "" 23 | }, 24 | "require": { 25 | "php": ">5.2.4" 26 | }, 27 | "suggest": { 28 | "composer/installers": "~1.0" 29 | }, 30 | "type": "wordpress-plugin", 31 | "notification-url": "https://packagist.org/downloads/", 32 | "license": [ 33 | "GPL-2.0+" 34 | ], 35 | "authors": [ 36 | { 37 | "name": "WebDevStudios", 38 | "email": "contact@webdevstudios.com", 39 | "homepage": "https://github.com/WebDevStudios", 40 | "role": "Developer" 41 | } 42 | ], 43 | "description": "Custom field for CMB2 which adds a post-search dialog for searching/attaching other post IDs.", 44 | "homepage": "https://github.com/WebDevStudios/CMB2", 45 | "keywords": [ 46 | "field", 47 | "metabox", 48 | "plugin", 49 | "search", 50 | "wordpress" 51 | ], 52 | "time": "2016-03-28 02:50:41" 53 | } 54 | ], 55 | "packages-dev": [], 56 | "aliases": [], 57 | "minimum-stability": "stable", 58 | "stability-flags": { 59 | "webdevstudios/cmb2-post-search-field": 20 60 | }, 61 | "prefer-stable": false, 62 | "prefer-lowest": false, 63 | "platform": { 64 | "php": ">5.2.4" 65 | }, 66 | "platform-dev": [] 67 | } 68 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function( grunt ) { 2 | 3 | require('load-grunt-tasks')(grunt); 4 | 5 | var pkg = grunt.file.readJSON( 'package.json' ); 6 | 7 | var bannerTemplate = '/**\n' + 8 | ' * <%= pkg.title %> - v<%= pkg.version %> - <%= grunt.template.today("yyyy-mm-dd") %>\n' + 9 | ' * <%= pkg.author.url %>\n' + 10 | ' *\n' + 11 | ' * Copyright (c) <%= grunt.template.today("yyyy") %>;\n' + 12 | ' * Licensed GPLv2+\n' + 13 | ' */\n'; 14 | 15 | var compactBannerTemplate = '/** ' + 16 | '<%= pkg.title %> - v<%= pkg.version %> - <%= grunt.template.today("yyyy-mm-dd") %> | <%= pkg.author.url %> | Copyright (c) <%= grunt.template.today("yyyy") %>; | Licensed GPLv2+' + 17 | ' **/\n'; 18 | 19 | // Project configuration 20 | grunt.initConfig( { 21 | 22 | pkg: pkg, 23 | 24 | 25 | watch: { 26 | styles: { 27 | files: ['assets/**/*.css','assets/**/*.scss'], 28 | tasks: ['styles'], 29 | options: { 30 | spawn: false, 31 | livereload: true, 32 | debounceDelay: 500 33 | } 34 | }, 35 | scripts: { 36 | files: ['assets/**/*.js'], 37 | tasks: ['scripts'], 38 | options: { 39 | spawn: false, 40 | livereload: true, 41 | debounceDelay: 500 42 | } 43 | }, 44 | php: { 45 | files: ['**/*.php', '!vendor/**.*.php'], 46 | tasks: ['php'], 47 | options: { 48 | spawn: false, 49 | debounceDelay: 500 50 | } 51 | } 52 | }, 53 | 54 | makepot: { 55 | dist: { 56 | options: { 57 | domainPath: '/languages/', 58 | potFilename: pkg.name + '.pot', 59 | type: 'wp-plugin' 60 | } 61 | } 62 | }, 63 | 64 | addtextdomain: { 65 | dist: { 66 | options: { 67 | textdomain: pkg.name 68 | }, 69 | target: { 70 | files: { 71 | src: ['**/*.php'] 72 | } 73 | } 74 | } 75 | }, 76 | 77 | // make a zipfile 78 | compress: { 79 | main: { 80 | options: { 81 | mode: 'zip', 82 | archive: 'cmb2-group-map.zip' 83 | }, 84 | files: [ { 85 | expand: true, 86 | // cwd: '/', 87 | src: [ 88 | '**', 89 | '!**/phpunit.xml', 90 | '!**/package.json', 91 | '!**/node_modules/**', 92 | '!**/bin/**', 93 | '!**/tests/**', 94 | '!**/sass/**', 95 | '!**.zip', 96 | '!**/**.orig', 97 | '!**/**.map', 98 | '!**/**Gruntfile.js', 99 | '!**/**composer.json', 100 | '!**/**composer.lock', 101 | '!**/**bower.json' 102 | ], 103 | dest: '/cmb2-group-map' 104 | } ] 105 | } 106 | }, 107 | 108 | githooks: { 109 | all: { 110 | // create zip and deploy changes to ftp 111 | 'pre-push': 'compress' 112 | } 113 | } 114 | } ); 115 | 116 | // Default task. 117 | grunt.registerTask( 'scripts', [] ); 118 | grunt.registerTask( 'styles', [] ); 119 | grunt.registerTask( 'php', [ 'addtextdomain', 'makepot' ] ); 120 | grunt.registerTask( 'default', ['styles', 'scripts', 'php', 'compress'] ); 121 | 122 | grunt.util.linefeed = '\n'; 123 | }; 124 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | CMB2 Group Map 2 | ====================== 3 | 4 | Originally created for [Post Status](https://poststatus.com/) to associate footnotes with notes, This CMB2 addon allows you to use CMB2 group fields to manage custom post type entries. 5 | 6 | You will either need to clone and then run `composer install` in this plugin's directory to get the required dependencies, or simply [download the compiled zip](https://github.com/zao-web/cmb2-group-map/raw/master/cmb2-group-map.zip). 7 | 8 | A few details: 9 | 10 | * To specify the post type destination, add a `'post_type_map'` parameter to your group field parameters. 11 | * To set the default WordPress post fields (e.g. the post content, title, etc), you need to set the `'id'` parameter to the same value as the `wp_posts` database column name. So for the content, the `'id'` parameter would be `'post_content'`, or for the title, `'post_title'`. 12 | * To set taxonomy terms for the connected post-type posts, use one of the `taxonomy_*` field types. 13 | 14 | ## Example 15 | 16 | You need to include this library: 17 | ```php 18 | require_once( 'cmb2-group-map/cmb2-group-map.php' ); 19 | ``` 20 | 21 | Then setup a CMB2 metabox with a grouped field: 22 | ```php 23 | // Standard CMB2 registration. 24 | $cmb = new_cmb2_box( array( 25 | 'id' => 'footnotes', 26 | 'title' => 'Footnotes', 27 | 'object_types' => array( 'post' ), 28 | ) ); 29 | 30 | // This is our group field registration. It's mostly standard. 31 | $group_field_id = $cmb->add_field( array( 32 | 'id' => 'footnotes_ids', 33 | 'type' => 'group', 34 | // This is a custom property, and should specify 35 | // the destination post-type. 36 | 'post_type_map' => 'footnotes', 37 | 'description' => 'Manage/add connected footnotes', 38 | 'options' => array( 39 | 'group_title' => 'Footnote {#}', 40 | 'add_button' => 'Add New Footnote', 41 | 'remove_button' => 'Remove Footnote', 42 | 'sortable' => true, 43 | ), 44 | ) ); 45 | 46 | // by using 'post_title' as the id, this will be the 47 | // value stored to the destination post-type's title field. 48 | $cmb->add_group_field( $group_field_id, array( 49 | 'name' => 'Title', 50 | 'id' => 'post_title', 51 | 'type' => 'text', 52 | ) ); 53 | 54 | // by using 'post_content' as the id, this will be the 55 | // value stored to the destination post-type's content field. 56 | $cmb->add_group_field( $group_field_id, array( 57 | 'name' => 'Content', 58 | 'id' => 'post_content', 59 | 'type' => 'textarea_small', 60 | ) ); 61 | 62 | // This field will be stored as post-meta against 63 | // the destination post-type. 64 | $cmb->add_group_field( $group_field_id, array( 65 | 'name' => 'Color meta value', 66 | 'id' => 'footnote_color', 67 | 'type' => 'colorpicker', 68 | ) ); 69 | 70 | // This field will be stored as category terms 71 | // against the destination post-type. 72 | $cmb->add_group_field( $group_field_id, array( 73 | 'name' => 'Categories', 74 | 'id' => 'category', 75 | 'taxonomy' => 'category', 76 | 'type' => 'taxonomy_multicheck', 77 | ) ); 78 | ``` 79 | 80 | ## Screenshots 81 | 82 | 1. Group Field Display 83 | ![Group Field Display](https://bytebucket.org/jtsternberg/cmb2-group-map/raw/34a20a8e0d31e0e3cc426f50ac05ee5cb19f52ce/screenshot-1.png?token=93e9beb2afc23a2ca5e7f48cf804a2672b60590e) 84 | 85 | 2. Group pulled from custom-post-type post 86 | ![Group pulled from custom-post-type post](https://bytebucket.org/jtsternberg/cmb2-group-map/raw/34a20a8e0d31e0e3cc426f50ac05ee5cb19f52ce/screenshot-2.png?token=73ef0c95689064f2ce20990d8153ced4481093ee) 87 | 88 | 3. Post-select search modal 89 | ![Post-select search modal](https://bytebucket.org/jtsternberg/cmb2-group-map/raw/34a20a8e0d31e0e3cc426f50ac05ee5cb19f52ce/screenshot-3.png?token=9fce8df6e720382cd5d2145257fe283d372ddd62) 90 | -------------------------------------------------------------------------------- /lib/base.php: -------------------------------------------------------------------------------- 1 | set_prop( 'original_object_type', $group_field->object_type ); 31 | $group_field->object_type( $group_field->args( 'object_type_map' ) ); 32 | } else { 33 | $group_field->args['original_object_type'] = $group_field->object_type; 34 | $group_field->object_type = $group_field->args( 'object_type_map' ); 35 | } 36 | 37 | $this->group_field = $group_field; 38 | } 39 | 40 | /** 41 | * Get the group field's object ID. 42 | * 43 | * @since 0.1.0 44 | * 45 | * @return int Object ID. 46 | */ 47 | public function object_id() { 48 | return $this->group_field->object_id; 49 | } 50 | 51 | /** 52 | * Get this group field's object type. 53 | * 54 | * @since 0.1.0 55 | * 56 | * @param string $check If set, checks if group field's object type matches value. 57 | * 58 | * @return string|bool Object type. Can be 'user', 'comment', 'term', or the 59 | * default 'post'. Or bool value from object type check. 60 | */ 61 | protected function object_type( $check = '' ) { 62 | $type = $this->group_field->object_type; 63 | if ( $check ) { 64 | return $check === $type; 65 | } 66 | return $type; 67 | } 68 | 69 | /** 70 | * Get the object for our object type 71 | * 72 | * @since 0.1.0 73 | * 74 | * @param int $object_id Object ID 75 | * 76 | * @return mixed Object instance if successful 77 | */ 78 | public function get_object( $object_id ) { 79 | switch ( $this->object_type() ) { 80 | case 'term': 81 | return get_term( $object_id, $this->group_field->args( 'taxonomy' ) ); 82 | case 'comment': 83 | return get_comment( $object_id ); 84 | case 'user': 85 | return get_user_by( 'id', $object_id ); 86 | default: 87 | return get_post( $object_id ); 88 | } 89 | } 90 | 91 | /** 92 | * Delete the object for our object type 93 | * 94 | * @since 0.1.0 95 | * 96 | * @param int $object_id Object ID 97 | * 98 | * @return mixed Object instance if successful 99 | */ 100 | public function delete_object( $object_id, $force_delete = true ) { 101 | switch ( $this->object_type() ) { 102 | case 'term': 103 | return wp_delete_term( $object_id, $this->group_field->args( 'taxonomy' ) ); 104 | case 'comment': 105 | return wp_delete_comment( $object_id, $force_delete ); 106 | case 'user': 107 | return wp_delete_user( $object_id ); 108 | default: 109 | return wp_delete_post( $object_id, $force_delete ); 110 | } 111 | } 112 | 113 | /** 114 | * CMB2_Group_Map::object_id_key which gets the unique ID field key for the object type. 115 | * 116 | * @since 0.1.0 117 | * 118 | * @return string ID field key 119 | */ 120 | public function object_id_key() { 121 | return CMB2_Group_Map::object_id_key( $this->object_type() ); 122 | } 123 | 124 | /** 125 | * Check if field id is one of the object type's default fields 126 | * 127 | * @since 0.1.0 128 | * 129 | * @param string $field_id Field id 130 | * 131 | * @return boolean 132 | */ 133 | public function is_object_field( $field_id ) { 134 | switch ( $this->object_type() ) { 135 | case 'term': 136 | return isset( CMB2_Group_Map::$term_fields[ $field_id ] ); 137 | case 'comment': 138 | return isset( CMB2_Group_Map::$comment_fields[ $field_id ] ); 139 | case 'user': 140 | return isset( CMB2_Group_Map::$user_fields[ $field_id ] ); 141 | default: 142 | return isset( CMB2_Group_Map::$post_fields[ $field_id ] ); 143 | } 144 | } 145 | 146 | /** 147 | * trigger_error wrapper which sets error level to E_USER_WARNING and returns empty string. 148 | * 149 | * @since 0.1.0 150 | * 151 | * @param string $msg Error string 152 | * 153 | * @return string Empty string for returning. 154 | */ 155 | protected function trigger_warning( $msg ) { 156 | trigger_error( $msg, E_USER_WARNING ); 157 | return ''; 158 | } 159 | 160 | /** 161 | * Magic getter for our object. 162 | * 163 | * @since 0.1.0 164 | * 165 | * @param string $field 166 | * @throws Exception Throws an exception if the field is invalid. 167 | * 168 | * @return mixed 169 | */ 170 | public function __get( $field ) { 171 | switch ( $field ) { 172 | case 'group_field': 173 | return $this->{$field}; 174 | default: 175 | throw new Exception( 'Invalid ' . __CLASS__ . ' property: ' . $field ); 176 | } 177 | } 178 | 179 | } 180 | -------------------------------------------------------------------------------- /cmb2-group-map.php: -------------------------------------------------------------------------------- 1 | 20 | * @copyright 2016 Zao 21 | * @license GPL-2.0+ 22 | * @version 0.1.0 23 | * @link https://github.com/zao-web/cmb2-group-map 24 | * @since 0.1.0 25 | */ 26 | 27 | /** 28 | * Copyright (c) 2016 Zao (email : jt@zao.is) 29 | * 30 | * This program is free software; you can redistribute it and/or modify 31 | * it under the terms of the GNU General Public License, version 2 or, at 32 | * your discretion, any later version, as published by the Free 33 | * Software Foundation. 34 | * 35 | * This program is distributed in the hope that it will be useful, 36 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 37 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 38 | * GNU General Public License for more details. 39 | * 40 | * You should have received a copy of the GNU General Public License 41 | * along with this program; if not, write to the Free Software 42 | * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 43 | */ 44 | 45 | /** 46 | * Loader versioning: http://jtsternberg.github.io/wp-lib-loader/ 47 | */ 48 | 49 | if ( ! class_exists( 'CMB2_Group_Map_010', false ) ) { 50 | 51 | /** 52 | * Versioned loader class-name 53 | * 54 | * This ensures each version is loaded/checked. 55 | * 56 | * @category WordPressLibrary 57 | * @package CMB2_Group_Map 58 | * @author Zao 59 | * @license GPL-2.0+ 60 | * @version 0.1.0 61 | * @link https://github.com/zao-web/cmb2-group-map 62 | * @since 0.1.0 63 | */ 64 | class CMB2_Group_Map_010 { 65 | 66 | /** 67 | * CMB2_Group_Map version number 68 | * @var string 69 | * @since 0.1.0 70 | */ 71 | const VERSION = '0.1.0'; 72 | 73 | /** 74 | * Current version hook priority. 75 | * Will decrement with each release 76 | * 77 | * @var int 78 | * @since 0.1.0 79 | */ 80 | const PRIORITY = 9999; 81 | 82 | /** 83 | * Starts the version checking process. 84 | * Creates CMB2_GROUP_POST_MAP_LOADED definition for early detection by 85 | * other scripts. 86 | * 87 | * Hooks CMB2_Group_Map inclusion to the cmb2_group_map_load hook 88 | * on a high priority which decrements (increasing the priority) with 89 | * each version release. 90 | * 91 | * @since 0.1.0 92 | */ 93 | public function __construct() { 94 | if ( ! defined( 'CMB2_GROUP_POST_MAP_LOADED' ) ) { 95 | /** 96 | * A constant you can use to check if CMB2_Group_Map is loaded 97 | * for your plugins/themes with CMB2_Group_Map dependency. 98 | * 99 | * Can also be used to determine the priority of the hook 100 | * in use for the currently loaded version. 101 | */ 102 | define( 'CMB2_GROUP_POST_MAP_LOADED', self::PRIORITY ); 103 | } 104 | 105 | // Use the hook system to ensure only the newest version is loaded. 106 | add_action( 'cmb2_group_map_load', array( $this, 'include_lib' ), self::PRIORITY ); 107 | 108 | /* 109 | * Hook in to the first hook we have available and 110 | * fire our `cmb2_group_map_load' hook. 111 | */ 112 | add_action( 'muplugins_loaded', array( __CLASS__, 'fire_hook' ), 9 ); 113 | add_action( 'plugins_loaded', array( __CLASS__, 'fire_hook' ), 9 ); 114 | add_action( 'after_setup_theme', array( __CLASS__, 'fire_hook' ), 9 ); 115 | } 116 | 117 | /** 118 | * Fires the cmb2_group_map_load action hook. 119 | * 120 | * @since 0.1.0 121 | */ 122 | public static function fire_hook() { 123 | if ( ! did_action( 'cmb2_group_map_load' ) ) { 124 | // Then fire our hook. 125 | do_action( 'cmb2_group_map_load' ); 126 | } 127 | } 128 | 129 | /** 130 | * A final check if CMB2_Group_Map exists before kicking off 131 | * our CMB2_Group_Map loading. 132 | * 133 | * CMB2_GROUP_POST_MAP_VERSION and CMB2_GROUP_POST_MAP_DIR constants are 134 | * set at this point. 135 | * 136 | * @since 0.1.0 137 | */ 138 | public function include_lib() { 139 | if ( class_exists( 'CMB2_Group_Map', false ) ) { 140 | return; 141 | } 142 | 143 | if ( ! defined( 'CMB2_GROUP_POST_MAP_VERSION' ) ) { 144 | /** 145 | * Defines the currently loaded version of CMB2_Group_Map. 146 | */ 147 | define( 'CMB2_GROUP_POST_MAP_VERSION', self::VERSION ); 148 | } 149 | 150 | if ( ! defined( 'CMB2_GROUP_POST_MAP_DIR' ) ) { 151 | /** 152 | * Defines the directory of the currently loaded version of CMB2_Group_Map. 153 | */ 154 | define( 'CMB2_GROUP_POST_MAP_DIR', dirname( __FILE__ ) . '/' ); 155 | } 156 | 157 | // Include and initiate CMB2_Group_Map. 158 | require_once CMB2_GROUP_POST_MAP_DIR . 'lib/init.php'; 159 | 160 | $file = CMB2_GROUP_POST_MAP_DIR . 'vendor/webdevstudios/cmb2-post-search-field/cmb2_post_search_field.php'; 161 | 162 | // If using composer, the post_search_field lib may be included elsewhere. 163 | if ( file_exists( $file ) ) { 164 | require_once $file; 165 | } 166 | } 167 | 168 | } 169 | 170 | // Kick it off. 171 | new CMB2_Group_Map_010; 172 | } 173 | -------------------------------------------------------------------------------- /lib/ajax.php: -------------------------------------------------------------------------------- 1 | throw_error( __LINE__, 'missing_required' ); 66 | } 67 | 68 | $post = get_post( absint( $post_data['post_id'] ) ); 69 | 70 | if ( ! $post ) { 71 | $this->throw_error( __LINE__, 'missing_required' ); 72 | } 73 | 74 | $host = get_post( absint( $post_data['host_id'] ) ); 75 | 76 | if ( ! $host ) { 77 | $this->throw_error( __LINE__, 'missing_required' ); 78 | } 79 | 80 | $this->host_id = $host->ID; 81 | $this->post_data = $post_data; 82 | $this->object_id = $post->ID; 83 | $this->group_fields = $group_fields; 84 | } 85 | 86 | /** 87 | * Handles sending input html data back to Javascript 88 | * 89 | * @since 0.1.0 90 | */ 91 | public function send_input_data() { 92 | $this->init_send_args(); 93 | $field = $this->find_group_field_array(); 94 | 95 | // Need to override the group id fetching and splice in our own new post id. 96 | add_filter( 'cmb2_group_map_get_group_ids', array( $this, 'override_ids_get' ), 10, 2 ); 97 | 98 | $group_field = new CMB2_Field( array( 99 | 'field_args' => $field, 100 | 'object_type' => 'post', 101 | 'object_id' => $this->host_id, 102 | ) ); 103 | $group_field->index = $this->index; 104 | 105 | parent::__construct( $group_field ); 106 | 107 | $inputs = array(); 108 | foreach ( $group_field->fields() as $index => $field_args ) { 109 | $field = new CMB2_Field( array( 110 | 'field_args' => $field_args, 111 | 'group_field' => $group_field, 112 | ) ); 113 | 114 | $field_type = new CMB2_Types( $field ); 115 | 116 | ob_start(); 117 | // Do html 118 | $field_type->render(); 119 | // grab the data from the output buffer and add it to our variable 120 | $html = ob_get_clean(); 121 | 122 | $inputs[ $index ] = array( 'html' => $html, 'type' => $field->type() ); 123 | } 124 | 125 | // Send it to JS. 126 | wp_send_json_success( $inputs ); 127 | } 128 | 129 | /** 130 | * Handles deleting a post, as requested by Javascript handler. 131 | * 132 | * @since 0.1.0 133 | */ 134 | public function delete() { 135 | if ( ! isset( $this->post_data['nonce'] ) ) { 136 | $this->throw_error( __LINE__, 'missing_nonce' ); 137 | } 138 | if ( ! isset( $this->post_data['group_id'] ) ) { 139 | $this->throw_error( __LINE__, 'missing_required' ); 140 | } 141 | 142 | $this->field_id = sanitize_text_field( $this->post_data['group_id'] ); 143 | $field = $this->find_group_field_array(); 144 | 145 | if ( ! wp_verify_nonce( $this->post_data['nonce'], $field['id'] ) ) { 146 | $this->throw_error( __LINE__, 'missing_nonce' ); 147 | } 148 | 149 | $group_field = new CMB2_Field( array( 150 | 'field_args' => $field, 151 | 'object_type' => 'post', 152 | 'object_id' => $this->host_id, 153 | ) ); 154 | 155 | parent::__construct( $group_field ); 156 | 157 | if ( $this->delete_object( $this->object_id ) ) { 158 | // Trigger an action after objects were updated/created 159 | do_action( 'cmb2_group_map_associated_object_deleted', $this->object_id, $this->host_id, $group_field ); 160 | 161 | wp_send_json_success(); 162 | } 163 | 164 | 165 | $this->throw_error( __LINE__, 'could_not_delete' ); 166 | } 167 | 168 | /** 169 | * Checks for correct data for fetching post data, and sets up variables. 170 | * 171 | * @since 0.1.0. 172 | */ 173 | protected function init_send_args() { 174 | if ( ! isset( $this->post_data['fieldName'] ) ) { 175 | $this->throw_error( __LINE__, 'missing_required' ); 176 | } 177 | 178 | $parts = explode( '[', sanitize_text_field( $this->post_data['fieldName'] ) ); 179 | $this->field_id = array_shift( $parts ); 180 | $index = explode( ']', array_shift( $parts ) ); 181 | $this->index = array_shift( $index ); 182 | } 183 | 184 | /** 185 | * Get the group field config array from the list of field groups. 186 | * 187 | * @since 0.1.0 188 | * 189 | * @return array Group field config array. 190 | */ 191 | protected function find_group_field_array() { 192 | foreach ( $this->group_fields as $cmb_id => $fields ) { 193 | foreach ( $fields as $_field_id => $field ) { 194 | if ( $_field_id === $this->field_id ) { 195 | $this->cmb_id = $cmb_id; 196 | return $field; 197 | } 198 | } 199 | } 200 | 201 | $this->throw_error( __LINE__, 'missing_required' ); 202 | } 203 | 204 | /** 205 | * Handles overriding the group ids and splices in our own new post id. 206 | * 207 | * @since 0.1.0 208 | * 209 | * @param array $object_ids Array of object ids. 210 | * @param CMB2_Field $group_field CMB2_Field group object. 211 | * 212 | * @return array Modified object ids. 213 | */ 214 | public function override_ids_get( $object_ids, CMB2_Field $group_field ) { 215 | $object_ids[ $this->index ] = $this->object_id; 216 | 217 | return $object_ids; 218 | } 219 | 220 | /** 221 | * Helper method to throw an exception using one of the CMB2_Group_Map $strings. 222 | * 223 | * @since 0.1.0 224 | * 225 | * @param int $line Line number 226 | * @param string $string_key The key to the string to use. 227 | */ 228 | protected function throw_error( $line, $string_key ) { 229 | throw new Exception( $line . ': '. CMB2_Group_Map::$strings[ $string_key ] ); 230 | } 231 | 232 | } 233 | -------------------------------------------------------------------------------- /lib/assets/js/cmb2-group-map.js: -------------------------------------------------------------------------------- 1 | window.CMB2Map = window.CMB2Map || {}; 2 | 3 | ( function( window, document, $, l10n, app, undefined ) { 4 | 'use strict'; 5 | 6 | /** 7 | * Kicked off when jQuery is ready. 8 | * 9 | * @since 0.1.0 10 | */ 11 | app.init = function() { 12 | if ( ! window.cmb2_post_search ) { 13 | return app.logError( 'CMB2 Post Search Field is required! https://github.com/WebDevStudios/CMB2-Post-Search-field' ); 14 | } 15 | 16 | // Make sure window.cmb2_post_search is setup 17 | setTimeout( app.overridePostSearch, 500 ); 18 | 19 | $( '.cmb2-group-map-group' ) 20 | // If removing a row, check if post should be deleted as well. 21 | .on( 'click', '.cmb-remove-group-row', app.maybeDelete ) 22 | // Move the post-search-field's button to somewhere visible 23 | .find( '.cmb-repeatable-grouping' ).each( app.moveButtons ); 24 | 25 | // And setup the click handler for that button. 26 | $( document.body ).on( 'click', '.cmb2-mapping-select, .cmb2-group-map-id-wrap', app.openSearch ); 27 | }; 28 | 29 | /** 30 | * Moves the cmb-post-search button and input next to the remove button 31 | * 32 | * @since 0.1.0 33 | */ 34 | app.moveButtons = function() { 35 | var $this = $( this ); 36 | var $id_row = $this.find( '.cmb2-group-map-id' ); 37 | 38 | var $btn = $id_row.find( '.cmb2-post-search-button' ).addClass( 'button cmb2-mapping-select' ); 39 | var $input = $id_row.find( 'input[type="text"]' ).prop( 'readonly', 1 ); 40 | var $span = $( ''+ $input.attr( 'title' ) +'' ); 41 | $span.append( app.sizeInput( $input ) ); 42 | 43 | $this.find( '.button.cmb-remove-group-row' ).after( $btn ).after( $span ); 44 | $id_row.remove(); 45 | }; 46 | 47 | /** 48 | * Opens the search modal. Is triggered when clicking on the search button/input. 49 | * 50 | * @since 0.1.0 51 | * 52 | * @param {object} evt Click event object. 53 | */ 54 | app.openSearch = function( evt ) { 55 | var search = window.cmb2_post_search; 56 | var $this = $( this ); 57 | // var $grouping = $this.parents( '.cmb-repeatable-grouping' ); 58 | var $input = $this.hasClass( 'cmb2-group-map-id-wrap' ) ? $this.find( 'input' ) : $this.prev().find( 'input' ); 59 | 60 | search.$idInput = $input; 61 | 62 | // Setup our variables from the field data 63 | $.extend( search, search.$idInput.data( 'search' ) ); 64 | 65 | search.trigger( 'open' ); 66 | }; 67 | 68 | /** 69 | * Handles overriding the cmb2_post_search handleSelected method, 70 | * new method fetches post data to populate group field inputs. 71 | * 72 | * @since 0.1.0 73 | */ 74 | app.overridePostSearch = function() { 75 | app.handleSelected = window.cmb2_post_search.handleSelected; 76 | 77 | // once a post is selected... 78 | window.cmb2_post_search.handleSelected = function( checked ) { 79 | if ( this.$idInput.hasClass( 'cmb2-group-map-data' ) ) { 80 | 81 | // ajax-grab the data we need 82 | app.get_and_set_post_data( checked[0], this.$idInput ); 83 | // Cache previous value in case we need to reset it. 84 | app.cachePrev = this.$idInput.val(); 85 | } 86 | 87 | // Fire standard method to update the post-search field's value. 88 | app.handleSelected.call( window.cmb2_post_search, checked ); 89 | app.sizeInput( this.$idInput ); 90 | }; 91 | }; 92 | 93 | /** 94 | * Resets the last-touched post search field's value, used when something went wrong. 95 | * 96 | * @since 0.1.0 97 | */ 98 | app.resetPostSearchField = function() { 99 | window.cmb2_post_search.$idInput.val( app.cachePrev ); 100 | app.sizeInput( window.cmb2_post_search.$idInput ); 101 | }; 102 | 103 | /** 104 | * Fetches post data to populate group field inputs. 105 | * 106 | * @since 0.1.0 107 | * 108 | * @param {int} post_id Post ID to get the data for. 109 | * @param {object} $input jQuery object for cmb2_post_search field input 110 | */ 111 | app.get_and_set_post_data = function( post_id, $input ) { 112 | var params = { 113 | action : 'cmb2_group_map_get_post_data', 114 | ajaxurl : l10n.ajaxurl, 115 | post_id : post_id, 116 | host_id : document.getElementById( 'post_ID' ).value, 117 | fieldName : $input.attr( 'name' ) 118 | }; 119 | 120 | var $group = $input.parents( '.cmb2-group-map-group .cmb-repeatable-grouping' ); 121 | 122 | // Ajax success handler 123 | var updateInputs = function( key, input ) { 124 | var $input = $( input.html ); 125 | var $replace = {}; 126 | var classesAttr; 127 | var nameAttr; 128 | var idAttr; 129 | 130 | // Special handling for certain types. 131 | if ( 'colorpicker' === input.type ) { 132 | $replace = $group.find( '.wp-picker-container' ); 133 | } 134 | // Try id first 135 | else if ( ( idAttr = $input.attr( 'id' ) ) ) { 136 | $replace = $( document.getElementById( idAttr ) ); 137 | } 138 | // Then try by class(es) 139 | else if ( ( classesAttr = $input.attr( 'class' ) ) ) { 140 | // Join classes to create a jQuery selector string. 141 | $replace = $group.find( '.' + classesAttr.split( /\s+/ ).join( '.' ) ); 142 | } 143 | // Finally, try by name attribute 144 | else if ( ( nameAttr = $input.attr( 'name' ) ) ) { 145 | $replace = $group.find( '[name="' + nameAttr + '"]' ); 146 | } 147 | 148 | // If we DIDN'T find an element to replace (we should have!) 149 | if ( ! $replace.length ) { 150 | app.logError( 'Could not find suitable selector in element.', [key, input] ); 151 | } 152 | 153 | // Ok, replace the element. 154 | $replace.replaceWith( $input ); 155 | 156 | // And fire CMB2's cleanup 157 | window.CMB2.afterRowInsert( $group ); 158 | }; 159 | 160 | // Fire ajax action to fetch new post/data. 161 | // If it fails, reset the post search field. 162 | $.post( l10n.ajaxurl, params, function( response ) { 163 | if ( ! response.success || ! response.data ) { 164 | app.resetPostSearchField(); 165 | return app.logError( response ); 166 | } 167 | 168 | $.each( response.data, updateInputs ); 169 | 170 | } ).fail( app.resetPostSearchField ); 171 | }; 172 | 173 | /** 174 | * When clicking "remove", check if user intends to delete 175 | * associated post. 176 | * 177 | * @since 0.1.0 178 | * 179 | * @param {object} evt Click event object 180 | */ 181 | app.maybeDelete = function( evt ) { 182 | evt.preventDefault(); 183 | 184 | var $btn = $( this ); 185 | var post_id = $btn.next().find( 'input' ).val(); 186 | 187 | // Check with user.. Delete the post as well? 188 | if ( post_id && window.confirm( l10n.strings.delete_permanent ) ) { 189 | app.doDelete( $btn, post_id ); 190 | } 191 | }; 192 | 193 | /** 194 | * Handles deleting the associated group's post object. 195 | * 196 | * @since 0.1.0 197 | * 198 | * @param {object} $btn jQuery object for the delete button. 199 | */ 200 | app.doDelete = function( $btn, post_id ) { 201 | var groupData = $btn.parents( '.cmb2-group-map-group' ).data(); 202 | 203 | var deleteFail = function() { 204 | app.logError( 'Sorry! unable to delete '+ post_id +'!' ); 205 | }; 206 | 207 | // Check if requirements are all here. 208 | if ( ! post_id || ! groupData.groupid ) { 209 | return deleteFail(); 210 | } 211 | 212 | var params = { 213 | action : 'cmb2_group_map_delete_item', 214 | ajaxurl : l10n.ajaxurl, 215 | post_id : post_id, 216 | host_id : document.getElementById( 'post_ID' ).value, 217 | nonce : groupData.nonce, 218 | group_id : groupData.groupid 219 | }; 220 | 221 | $.post( l10n.ajaxurl, params, function( response ) { 222 | if ( ! response.success ) { 223 | deleteFail(); 224 | 225 | if ( response.data ) { 226 | app.logError( response.data ); 227 | } 228 | } 229 | } ).fail( deleteFail ); 230 | }; 231 | 232 | /** 233 | * Set the size of the input to the length of its value. 234 | * 235 | * @since 0.1.0 236 | * 237 | * @param {object} $input jQuery object for the input. 238 | * 239 | * @return {object} jQuery object for the input. 240 | */ 241 | app.sizeInput = function( $input ) { 242 | var size = $input.val().length; 243 | // Set width of input to the size of its value or 4. 244 | return $input.prop( 'size', size > 4 ? size : 4 ); 245 | }; 246 | 247 | /** 248 | * Logs errors to the console. 249 | * 250 | * @since 0.1.0 251 | */ 252 | app.logError = function() { 253 | app.logError.history = app.logError.history || []; 254 | app.logError.history.push( arguments ); 255 | if ( window.console ) { 256 | window.console.error( Array.prototype.slice.call( arguments) ); 257 | } 258 | }; 259 | 260 | // kick it off. 261 | $( app.init ); 262 | 263 | } )( window, document, jQuery, window.CMB2Mapl10n, window.CMB2Map ); 264 | -------------------------------------------------------------------------------- /lib/set.php: -------------------------------------------------------------------------------- 1 | value = $value; 20 | } 21 | 22 | /** 23 | * Handles saving the group field value out to individual mapped-type objects. 24 | * Calls 'cmb2_group_map_updated' hook. 25 | * 26 | * @since 0.1.0 27 | */ 28 | public function save() { 29 | $updated = array(); 30 | $group_field = $this->group_field; 31 | $parent_object_id = $group_field->object_id; 32 | 33 | if ( ! empty( $this->value ) ) { 34 | 35 | $field_id = $group_field->id( true ); 36 | $fields = array(); 37 | $form_data = isset( $_POST[ $field_id ] ) ? $_POST[ $field_id ] : array(); 38 | 39 | $count = 0; 40 | foreach ( (array) $group_field->args( 'fields' ) as $field_args ) { 41 | $field = new CMB2_Field( array( 42 | 'field_args' => $field_args, 43 | 'group_field' => $group_field, 44 | ) ); 45 | 46 | $fields[ $field->id( true ) ] = $field; 47 | $count++; 48 | } 49 | 50 | foreach ( $this->value as $index => $clean ) { 51 | $data = $this->object_data( $fields, $clean, $form_data[ $index ] ); 52 | 53 | if ( $object_id = $this->update_or_insert( $data ) ) { 54 | $updated[ $index ] = $object_id; 55 | } 56 | } 57 | } 58 | 59 | // Trigger an action after objects were updated/created 60 | do_action( 'cmb2_group_map_updated', $updated, $parent_object_id, $group_field ); 61 | } 62 | 63 | /** 64 | * The object data for a object import. 65 | * 66 | * @since 0.1.0 67 | * 68 | * @param array $fields Array of group sub-fields. 69 | * @param array $clean_vals Array of CMB2-cleaned values for this object/group. 70 | * @param array $unclean Array of $_POST values for the group. 71 | * 72 | * @return array Array of object data for the import 73 | */ 74 | protected function object_data( $fields, $clean_vals, $unclean ) { 75 | $data = array(); 76 | 77 | foreach ( $fields as $sub_field_id => $field ) { 78 | $unclean_value = isset( $unclean[ $sub_field_id ] ) 79 | ? $unclean[ $sub_field_id ] 80 | : null; 81 | 82 | $data = $this->add_field_data( 83 | $data, 84 | $field, 85 | $clean_vals, 86 | $unclean_value 87 | ); 88 | } 89 | 90 | // Set some default object data params 91 | if ( ! empty( $data ) ) { 92 | $data = $this->set_default_data( $data ); 93 | } 94 | 95 | return apply_filters( 'cmb2_group_map_set_object_data', $data, $this ); 96 | } 97 | 98 | /** 99 | * Add data from the fields to the array of object data. 100 | * 101 | * @since 0.1.0 102 | * 103 | * @param array $object_data Array of object data for the import. 104 | * @param CMB2_Field $field Sub-field object. 105 | * @param array $clean_vals Array of CMB2-cleaned values for this object/group. 106 | * @param mixed $unclean $_POST value for this field. 107 | * 108 | * @return array Maybe-modified Array of object data for the import. 109 | */ 110 | public function add_field_data( $object_data, $field, $clean_vals, $unclean ) { 111 | $clean_val = false; 112 | $field_id = $field->id( true ); 113 | $taxonomy = $field->args( 'taxonomy' ); 114 | 115 | if ( 'ID' === $field_id ) { 116 | 117 | // $unclean value is the object ID. 118 | $clean_val = absint( $unclean ); 119 | 120 | } elseif ( $taxonomy ) { 121 | 122 | // Get taxonomy values from the $_POST data, and clean them. 123 | $clean_val = is_array( $unclean ) ? array_map( 'sanitize_text_field', $unclean ) : sanitize_text_field( $unclean ); 124 | 125 | } elseif ( isset( $clean_vals[ $field_id ] ) ) { 126 | $clean_val = $clean_vals[ $field_id ]; 127 | } 128 | 129 | if ( ! $clean_val ) { 130 | // Could not a find a clean value?! 131 | return $object_data; 132 | } 133 | 134 | // If the field id matches a object field 135 | if ( $this->is_object_field( $field_id ) ) { 136 | 137 | // Then apply it directly. 138 | $object_data[ $field_id ] = $clean_val; 139 | } elseif ( $taxonomy ) { 140 | 141 | // If the field has a taxonomy parameter, then set value to that taxonomy 142 | // @todo figure out why unchecking all terms does not remove them. 143 | $object_data['tax_input'][ $taxonomy ] = $clean_val; 144 | 145 | } else { 146 | 147 | // And finally, apply the rest as object meta. 148 | $object_data['meta_input'][ $field_id ] = $clean_val; 149 | } 150 | 151 | return $object_data; 152 | } 153 | 154 | /** 155 | * If object data exists, then set some default values for the object 156 | * 157 | * @since 0.1.0 158 | * 159 | * @param array $data Array of modified object data. 160 | */ 161 | public function set_default_data( $data ) { 162 | $key = $this->object_id_key(); 163 | 164 | // If we only have an id, unset that and return (to unattach a post) 165 | if ( 1 === count( $data ) && ! empty( $data[ $key ] ) ) { 166 | unset( $data[ $key ] ); 167 | 168 | return $data; 169 | } 170 | 171 | switch ( $this->object_type() ) { 172 | case 'user': 173 | $data['ID'] = isset( $data['ID'] ) ? $data['ID'] : 0; 174 | $data['role'] = isset( $data['role'] ) ? $data['role'] : 'subscriber'; 175 | break; 176 | 177 | case 'comment': 178 | $data['comment_post_ID'] = $this->group_field->object_id; 179 | $data['user_id'] = isset( $data['user_id'] ) ? $data['user_id'] : get_current_user_id(); 180 | $data['comment_ID'] = isset( $data['comment_ID'] ) ? $data['comment_ID'] : 0; 181 | break; 182 | 183 | case 'term': 184 | $data['taxonomy'] = $this->group_field->args( 'taxonomy' ); 185 | $data['term_id'] = isset( $data['term_id'] ) ? $data['term_id'] : 0; 186 | $data['term'] = isset( $data['term'] ) ? $data['term'] : ''; 187 | break; 188 | 189 | default: 190 | $data['post_type'] = $this->group_field->args( 'post_type_map' ); 191 | $data['post_status'] = get_post_status( $this->group_field->object_id ); 192 | $data['post_date'] = get_post_field( 'post_date', $this->group_field->object_id, 'db' ); 193 | $data['ID'] = isset( $data['ID'] ) ? $data['ID'] : 0; 194 | break; 195 | } 196 | 197 | return $data; 198 | } 199 | 200 | /** 201 | * If the object data array is not empty, either create or update a object 202 | * in the mapped object-type. 203 | * 204 | * @since 0.1.0 205 | * 206 | * @param array $data Array of object data for the import. 207 | * 208 | * @return mixed Result of update, insert or is false if no data. 209 | */ 210 | protected function update_or_insert( $data ) { 211 | if ( empty( $data ) ) { 212 | return false; 213 | } 214 | 215 | // Update object? 216 | $updated = $this->update( $data ); 217 | if ( 'no' !== $updated ) { 218 | return $updated; 219 | } 220 | 221 | // Or create it? 222 | return $this->insert( $data ); 223 | } 224 | 225 | /** 226 | * Possibly update an object. 227 | * 228 | * @since 0.1.0 229 | * 230 | * @param array $data Array of object data for the update. 231 | * 232 | * @return mixed Result of update, or 'no' if the checks failed. 233 | */ 234 | protected function update( $data ) { 235 | $updated = 'no'; 236 | 237 | switch ( $this->object_type() ) { 238 | case 'user': 239 | if ( isset( $data['ID'] ) && $data['ID'] && get_user_by( 'id', $data['ID'] ) ) { 240 | $updated = wp_update_user( $data ); 241 | } 242 | break; 243 | 244 | case 'comment': 245 | if ( isset( $data['comment_ID'] ) && $data['comment_ID'] && get_comment( $data['comment_ID'] ) ) { 246 | $updated = wp_update_post( $data ); 247 | } 248 | break; 249 | 250 | case 'term': 251 | if ( isset( $data['term_id'] ) && $data['term_id'] && get_term( $data['term_id'], $data['taxonomy'] ) ) { 252 | $updated = wp_update_term( $data['term_id'], $data['taxonomy'], $data ); 253 | } 254 | break; 255 | 256 | default: 257 | if ( isset( $data['ID'] ) && $data['ID'] && get_post( $data['ID'] ) ) { 258 | $updated = wp_update_post( $data ); 259 | } 260 | break; 261 | } 262 | 263 | return $updated; 264 | } 265 | 266 | /** 267 | * Insert an object. 268 | * 269 | * @since 0.1.0 270 | * 271 | * @param array $data Array of object data for the insert. 272 | * 273 | * @return mixed Result of insert. 274 | */ 275 | protected function insert( $data ) { 276 | switch ( $this->object_type() ) { 277 | case 'user': 278 | if ( isset( $data['ID'] ) ) { 279 | unset( $data['ID'] ); 280 | } 281 | 282 | return wp_insert_user( $data ); 283 | 284 | case 'comment': 285 | if ( isset( $data['comment_ID'] ) ) { 286 | unset( $data['comment_ID'] ); 287 | } 288 | 289 | return wp_insert_comment( $data ); 290 | 291 | case 'term': 292 | if ( isset( $data['term_id'] ) ) { 293 | unset( $data['term_id'] ); 294 | } 295 | 296 | return wp_insert_term( $data['term'], $data['taxonomy'], $data ); 297 | 298 | default: 299 | if ( isset( $data['ID'] ) ) { 300 | unset( $data['ID'] ); 301 | } 302 | 303 | return wp_insert_post( $data ); 304 | } 305 | } 306 | 307 | } 308 | -------------------------------------------------------------------------------- /lib/get.php: -------------------------------------------------------------------------------- 1 | group ? $field->group->id() : $field->id(); 62 | 63 | if ( 'group' !== $field->type() ) { 64 | if ( ! $field->group ) { 65 | return $this->trigger_warning( __METHOD__ . ' only works with group fields.' ); 66 | } 67 | 68 | if ( ! isset( self::$getters[ $field_id ] ) ) { 69 | return $this->trigger_warning( 'Something went wrong! The group field needs to have already set up the object.' ); 70 | } 71 | 72 | $getter = self::$getters[ $field_id ]; 73 | $value = $getter->subfield_value( $field ); 74 | 75 | // Return filtered value. 76 | return apply_filters( 'cmb2_group_map_get_subfield_value', $value, $getter, $field ); 77 | } 78 | 79 | if ( ! isset( self::$getters[ $field_id ] ) ) { 80 | self::$getters[ $field_id ] = new self( $field ); 81 | } 82 | 83 | $getter = self::$getters[ $field_id ]; 84 | $value = $getter->group_field_value(); 85 | 86 | // Return filtered value. 87 | return apply_filters( 'cmb2_group_map_get_group_field_value', $value, $getter ); 88 | } 89 | 90 | /** 91 | * Constructor. Setup the getter object. 92 | * 93 | * @since 0.1.0 94 | * 95 | * @param CMB2_Field $group_field Group field to get the values for. 96 | */ 97 | public function __construct( CMB2_Field $group_field ) { 98 | 99 | // Get meta before we change the group field's object type (in the parent constructor) 100 | $object_ids = CMB2_Group_Map::get_map_meta( $group_field ); 101 | 102 | $object_ids = apply_filters( 'cmb2_group_map_get_group_ids', $object_ids, $group_field ); 103 | if ( is_array( $object_ids ) ) { 104 | $object_ids = array_filter( $object_ids, 'get_post' ); 105 | $this->object_ids = array_values( $object_ids ); 106 | } else { 107 | $this->object_ids = array(); 108 | } 109 | 110 | parent::__construct( $group_field ); 111 | } 112 | 113 | /** 114 | * Get the group field value 115 | * 116 | * @since 0.1.0 117 | * 118 | * @return array Array of values for the group. 119 | */ 120 | public function group_field_value() { 121 | if ( null !== $this->value ) { 122 | return $this->value; 123 | } 124 | 125 | $this->value = array(); 126 | 127 | if ( empty( $this->object_ids ) ) { 128 | return $this->value; 129 | } 130 | 131 | $all_fields = $this->group_field->fields(); 132 | $stored_id = $this->group_field->object_id; 133 | 134 | foreach ( $this->object_ids as $this->group_field->index => $object_id ) { 135 | 136 | // Only proceed if there is an actual object by this id 137 | if ( $this->object = $this->get_object( $object_id ) ) { 138 | 139 | // Temp. set the group field's object id to this object id 140 | $this->set_group_field_object_id( $object_id ); 141 | 142 | // initiate the cached values for this object id 143 | $this->value[ $this->group_field->index ] = array(); 144 | 145 | // loop the group field's sub-fields 146 | foreach ( $all_fields as $field_id => $field_args ) { 147 | 148 | // And set the override filter for value-getting 149 | add_filter( 'cmb2_override_meta_value', array( $this, 'set_sub_field_value' ), 9, 4 ); 150 | 151 | // Then init our field object, which will fetch the value 152 | // (and cache it for future initiations/lookups) 153 | $subfield = new CMB2_Field( array( 154 | 'field_args' => $field_args, 155 | 'group_field' => $this->group_field, 156 | ) ); 157 | } 158 | } 159 | } 160 | 161 | // Restore the group field object id 162 | $this->set_group_field_object_id( $stored_id ); 163 | 164 | // Return the full value array 165 | return $this->value; 166 | } 167 | 168 | /** 169 | * Gets value for subfield from the value for the whole group. 170 | * 171 | * @since 0.1.0 172 | * 173 | * @param CMB2_Field $subfield CMB2_Field 174 | * 175 | * @return mixed Value for subfield. 176 | */ 177 | public function subfield_value( CMB2_Field $subfield ) { 178 | $value = $this->group_field_value(); 179 | 180 | // No sub-field? 181 | if ( empty( $value[ $this->group_field->index ] ) ) { 182 | return null; 183 | } 184 | 185 | $field_index = $this->group_field->index; 186 | $field_id = $subfield->id( true ); 187 | 188 | return isset( $value[ $field_index ][ $field_id ] ) 189 | ? $value[ $field_index ][ $field_id ] 190 | : null; 191 | } 192 | 193 | /** 194 | * Hooks to 'cmb2_override_meta_value' and returns the value array we've created. 195 | * Also, if field is a taxonomy field, filter 'get_the_terms' to return the cached value. 196 | * 197 | * @since 0.1.0 198 | * 199 | * @param mixed $nooverride Value w/o override. 200 | * @param int $object_id Object ID 201 | * @param array $args Array of field arguments. 202 | * @param CMB2_Field $subfield Sub-field object. 203 | * 204 | * @return array Array of values for the group. 205 | */ 206 | public function set_sub_field_value( $nooverride, $object_id, $args, CMB2_Field $subfield ) { 207 | $field_id = $subfield->id( true ); 208 | 209 | if ( 210 | // If we already have this value 211 | isset( $this->value[ $this->group_field->index ][ $field_id ] ) 212 | // And this is def. a subfield 213 | && $subfield->group instanceof CMB2_Field 214 | // And the parent/group field's ID matches the one on our object 215 | && $this->group_field->id() === $subfield->group->id() 216 | ) { 217 | 218 | // Maybe do extra magic to get taxonomy term cached values 219 | $this->check_taxonomy_cache( $subfield ); 220 | 221 | // Then return the already existing value 222 | return $this->value; 223 | } 224 | 225 | $subfield_value = null; 226 | 227 | // If the field id matches a object field 228 | if ( $this->is_object_field( $field_id ) ) { 229 | $subfield_value = $this->object->{ $field_id }; 230 | } 231 | 232 | // If the field has a taxonomy parameter, then get value from that taxonomy 233 | elseif ( $subfield->args( 'taxonomy' ) ) { 234 | $subfield_value = $this->get_value_from_taxonomy( $subfield ); 235 | } 236 | 237 | // And finally, get the data from the object meta. 238 | else { 239 | $single = $subfield->args( 'repeatable' ) || ! $subfield->args( 'multiple' ); 240 | 241 | $subfield_value = get_metadata( 242 | $this->object_type(), 243 | $this->object_id(), 244 | $field_id, 245 | $single 246 | ); 247 | } 248 | 249 | $this->value[ $this->group_field->index ][ $field_id ] = $subfield_value; 250 | 251 | return $this->value; 252 | } 253 | 254 | /** 255 | * Get the value from a taxonomy subfield 256 | * 257 | * @since 0.1.0 258 | * 259 | * @param CMB2_Field $subfield Subfield object. 260 | * 261 | * @return mixed Array of terms if successful. 262 | */ 263 | public function get_value_from_taxonomy( CMB2_Field $subfield ) { 264 | // No taxonomies for taxonomies 265 | if ( $this->object_type( 'term' ) ) { 266 | return null; 267 | } 268 | 269 | $taxonomy = $subfield->args( 'taxonomy' ); 270 | $terms = get_the_terms( $this->object, $taxonomy ); 271 | if ( ! $terms ) { 272 | $terms = array(); 273 | } 274 | 275 | // Cache this taxonomy's terms against the object ID. 276 | $this->terms[ $this->object_id() ][ $taxonomy ] = $terms; 277 | 278 | if ( is_wp_error( $terms ) || empty( $terms ) ) { 279 | 280 | // Fallback to default (if it's set) 281 | $terms = $subfield->args( 'default' ); 282 | 283 | } else { 284 | 285 | $terms = 'taxonomy_multicheck' === $subfield->type() 286 | ? wp_list_pluck( $terms, 'slug' ) 287 | : $terms[ key( $terms ) ]->slug; 288 | } 289 | 290 | return $terms ? $terms : array(); 291 | } 292 | 293 | /** 294 | * Checks if subfield is a taxonomy field, and then filters 'get_the_terms' 295 | * to return the cached value. 296 | * 297 | * @since 0.1.0 298 | * 299 | * @param CMB2_Field $subfield Sub-field object 300 | */ 301 | public function check_taxonomy_cache( CMB2_Field $subfield ) { 302 | 303 | // No taxonomies for taxonomies 304 | if ( $this->object_type( 'term' ) ) { 305 | return; 306 | } 307 | 308 | // If we're looking at a taxonomy field type, we have to do extra magic 309 | if ( $taxonomy = $subfield->args( 'taxonomy' ) ) { 310 | 311 | if ( ! isset( $this->object_ids[ $this->group_field->index ] ) ) { 312 | return; 313 | } 314 | 315 | // Get the next object id by removing it from the front of the object ids 316 | $this->term_object_id = $this->object_ids[ $this->group_field->index ]; 317 | 318 | // If we have a cached value for this object id, then proceed 319 | if ( 320 | isset( $this->terms[ $this->term_object_id ][ $taxonomy ] ) 321 | && $this->terms[ $this->term_object_id ][ $taxonomy ] 322 | ) { 323 | // We have a cached value, so we'll filter get_the_terms to return the cached value. 324 | add_filter( 'get_the_terms', array( $this, 'override_term_get' ), 10, 3 ); 325 | } 326 | } 327 | } 328 | 329 | /** 330 | * Hooked into 'get_the_terms', returns cached term array. 331 | * 332 | * @since 0.1.0 333 | * 334 | * @param array $terms Array of term objects 335 | * @param int $object_id Object ID. 336 | * @param string $taxonomy Taxonomy slug. 337 | * 338 | * @return array Array of term objects. 339 | */ 340 | public function override_term_get( $terms, $object_id, $taxonomy ) { 341 | // Final check if we do actually have the cached value 342 | if ( $this->term_object_id && isset( $this->terms[ $this->term_object_id ][ $taxonomy ] ) ) { 343 | // Ok we do, so let's return that instead. 344 | $terms = $this->terms[ $this->term_object_id ][ $taxonomy ]; 345 | } 346 | 347 | return $terms; 348 | } 349 | 350 | /** 351 | * The Group Field's object ID setter to make compatible w/ older CMB2. 352 | * 353 | * @since 0.1.0 354 | * 355 | * @param int $object_id The object ID to set. 356 | */ 357 | protected function set_group_field_object_id( $object_id ) { 358 | if ( class_exists( 'CMB2_Base' ) ) { 359 | $this->group_field->object_id( $object_id ); 360 | } else { 361 | $this->group_field->object_id = $object_id; 362 | } 363 | } 364 | 365 | } 366 | -------------------------------------------------------------------------------- /lib/init.php: -------------------------------------------------------------------------------- 1 | '', 51 | 'post_author' => '', 52 | 'post_date' => '', 53 | 'post_date_gmt' => '', 54 | 'post_content' => '', 55 | 'post_content_filtered' => '', 56 | 'post_title' => '', 57 | 'post_excerpt' => '', 58 | 'post_status' => '', 59 | 'post_type' => '', 60 | 'comment_status' => '', 61 | 'ping_status' => '', 62 | 'post_password' => '', 63 | 'post_name' => '', 64 | 'to_ping' => '', 65 | 'pinged' => '', 66 | 'post_modified' => '', 67 | 'post_modified_gmt' => '', 68 | 'post_parent' => '', 69 | 'menu_order' => '', 70 | 'post_mime_type' => '', 71 | 'guid' => '', 72 | 'tax_input' => '', 73 | 'meta_input' => '', 74 | ); 75 | 76 | /** 77 | * Native user fields 78 | * 79 | * @var array 80 | */ 81 | public static $user_fields = array( 82 | 'ID' => '', 83 | 'user_pass' => '', 84 | 'user_login' => '', 85 | 'user_nicename' => '', 86 | 'user_url' => '', 87 | 'user_email' => '', 88 | 'display_name' => '', 89 | 'nickname' => '', 90 | 'first_name' => '', 91 | 'last_name' => '', 92 | 'description' => '', 93 | 'rich_editing' => '', 94 | 'comment_shortcuts' => '', 95 | 'admin_color' => '', 96 | 'use_ssl' => '', 97 | 'user_registered' => '', 98 | 'show_admin_bar_front' => '', 99 | 'role' => '', 100 | ); 101 | 102 | /** 103 | * Native comment fields 104 | * 105 | * @var array 106 | */ 107 | public static $comment_fields = array( 108 | 'comment_agent' => '', 109 | 'comment_approved' => '', 110 | 'comment_author' => '', 111 | 'comment_author_email' => '', 112 | 'comment_author_IP' => '', 113 | 'comment_author_url' => '', 114 | 'comment_content' => '', 115 | 'comment_date' => '', 116 | 'comment_date_gmt' => '', 117 | 'comment_karma' => '', 118 | 'comment_parent' => '', 119 | 'comment_post_ID' => '', 120 | 'comment_type' => '', 121 | 'comment_meta' => '', 122 | 'user_id' => '', 123 | ); 124 | 125 | /** 126 | * Native term fields 127 | * 128 | * @var array 129 | */ 130 | public static $term_fields = array( 131 | 'term' => '', 132 | 'taxonomy' => '', 133 | 'alias_of' => '', 134 | 'description' => '', 135 | 'parent' => '', 136 | 'slug' => '', 137 | ); 138 | 139 | /** 140 | * Library strings, filtered through cmb2_group_map_strings for translation. 141 | * 142 | * @var array 143 | */ 144 | public static $strings = array( 145 | 'missing_required' => 'Missing required data.', 146 | 'missing_nonce' => 'Missing required validation nonce or failed nonce validation.', 147 | 'delete_permanent' => 'This item will be detached from this post. Do you want to also delete it permanently?', 148 | 'could_not_delete' => 'The item could not be deleted.', 149 | 'item_id' => '%s ID:', 150 | ); 151 | 152 | /** 153 | * Creates or returns an instance of this class. 154 | * @since 0.1.0 155 | * @return CMB2_Group_Map A single instance of this class. 156 | */ 157 | public static function get_instance() { 158 | if ( null === self::$single_instance ) { 159 | self::$single_instance = new self(); 160 | } 161 | 162 | return self::$single_instance; 163 | } 164 | 165 | /** 166 | * Constructor. 167 | * 168 | * @since 0.1.0 169 | */ 170 | protected function __construct() { 171 | add_action( 'cmb2_after_init', array( $this, 'setup_mapped_group_fields' ) ); 172 | add_action( 'cmb2_group_map_updated', array( $this, 'map_to_original_object' ), 10, 3 ); 173 | add_action( 'cmb2_group_map_associated_object_deleted', array( $this, 'remove_from_original_object' ), 10, 3 ); 174 | add_action( 'before_delete_post', array( $this, 'delete_associated_objects' ) ); 175 | add_action( 'wp_ajax_cmb2_group_map_get_post_data', array( $this, 'get_ajax_input_data' ) ); 176 | add_action( 'wp_ajax_cmb2_group_map_delete_item', array( $this, 'ajax_delete_item' ) ); 177 | } 178 | 179 | /** 180 | * Get all instances of group fields for mapping, and initiate them. 181 | * 182 | * @since 0.1.0 183 | */ 184 | public function setup_mapped_group_fields() { 185 | 186 | /** 187 | * Library's strings made available for translation. 188 | * 189 | * function cmb2_group_map_strings_i18n( $strings ) { 190 | * $strings['missing_required'] = __( 'Missing required data.', 'your-textdomain' ); 191 | * return $strings; 192 | * } 193 | * add_filter( 'cmb2_group_map_strings', 'cmb2_group_map_strings_i18n' ); 194 | * 195 | * @param array $strings Array of unmodified strings. 196 | * @return array Array of modified strings 197 | */ 198 | self::$strings = apply_filters( 'cmb2_group_map_strings', self::$strings ); 199 | 200 | foreach ( CMB2_Boxes::get_all() as $cmb ) { 201 | foreach ( (array) $cmb->prop( 'fields' ) as $field ) { 202 | if ( 203 | 'group' === $field['type'] 204 | && ( 205 | isset( $field['post_type_map'] ) 206 | && post_type_exists( $field['post_type_map'] ) 207 | ) 208 | || isset( $field['object_type_map'] ) 209 | ) { 210 | $this->setup_mapped_group_field( $cmb, $field ); 211 | } 212 | } 213 | } 214 | } 215 | 216 | /** 217 | * Initiate the mapping field group. 218 | * 219 | * @since 0.1.0 220 | * 221 | * @param CMB2 $cmb CMB2 instance. 222 | * @param array $field Group field config array. 223 | */ 224 | protected function setup_mapped_group_field( CMB2 $cmb, array $field ) { 225 | // Helpful reference back to the CMB object. 226 | $field['cmb_id'] = $cmb->cmb_id; 227 | 228 | $field = $this->set_object_type( $cmb, $field ); 229 | 230 | $this->set_before_after_group_hooks( $cmb, $field ); 231 | 232 | $field['original_object_types'] = $cmb->prop( 'object_types' ); 233 | 234 | $cmb->update_field_property( $field['id'], 'original_object_types', $field['original_object_types'] ); 235 | 236 | $cpt = get_post_type_object( $field['post_type_map'] ); 237 | 238 | // Add a hidden ID field to the group to store the referenced object id. 239 | $cmb->add_group_field( $field['id'], array( 240 | 'id' => self::object_id_key( $field['object_type_map'] ), 241 | 'type' => 'post_search_text', 242 | 'post_type' => $field['post_type_map'], 243 | 'select_type' => 'radio', 244 | 'select_behavior' => 'replace', 245 | 'row_classes' => 'hidden cmb2-group-map-id', 246 | 'options' => array( 247 | 'find_text' => $cpt->labels->search_items, 248 | ), 249 | 'attributes' => array( 250 | 'class' => 'regular-text cmb2-group-map-data', 251 | 'title' => sprintf( self::$strings['item_id'], $cpt->labels->singular_name ), 252 | ), 253 | ) ); 254 | 255 | $this->hook_cmb2_overrides( $field['id'] ); 256 | 257 | // Store fields to object property for retrieval (if necessary) 258 | $this->group_fields[ $cmb->cmb_id ][ $field['id'] ] = $field; 259 | } 260 | 261 | /** 262 | * Get/Set the object_type_map param. 263 | * 264 | * @since 0.1.0 265 | * 266 | * @param CMB2 $cmb CMB2 instance. 267 | * @param array $field Group field config array. 268 | */ 269 | protected function set_object_type( CMB2 $cmb, array $field ) { 270 | // Set object type 271 | if ( ! isset( $field['object_type_map'] ) || ! in_array( $field['object_type_map'], $this->allowed_object_types, 1 ) ) { 272 | $field['object_type_map'] = 'post'; 273 | } 274 | 275 | $cmb->update_field_property( $field['id'], 'object_type_map', $field['object_type_map'] ); 276 | 277 | if ( 'term' === $field['object_type_map'] && ( ! isset( $field['taxonomy'] ) || ! taxonomy_exists( $field['taxonomy'] ) ) ) { 278 | wp_die( 'Using "term" for the "object_type_map" parameter requires a "taxonomy" parameter to also be set.' ); 279 | } 280 | 281 | return $field; 282 | } 283 | 284 | /** 285 | * Set the before/after group callbacks and cache/store any existing callbacks, 286 | * to keep from stomping them. 287 | * 288 | * @since 0.1.0 289 | * 290 | * @param CMB2 $cmb CMB2 instance. 291 | * @param array $field Group field config array. 292 | */ 293 | protected function set_before_after_group_hooks( CMB2 $cmb, array $field ) { 294 | // Let's be sure not to stomp out any existing before_group/after_group parameters. 295 | if ( isset( $field['before_group'] ) ) { 296 | // Store them to another field property 297 | $cmb->update_field_property( $field['id'], 'cmb2_group_map_before_group', $field['before_group'] ); 298 | } 299 | if ( isset( $field['after_group'] ) ) { 300 | $cmb->update_field_property( $field['id'], 'cmb2_group_map_after_group', $field['after_group'] ); 301 | } 302 | 303 | // Hook in our JS registration using after_group group field parameter. 304 | // This ensures the enqueueing/registering only occurs if the field is displayed. 305 | $cmb->update_field_property( $field['id'], 'after_group', array( $this, 'after_group' ) ); 306 | $cmb->update_field_property( $field['id'], 'before_group', array( $this, 'before_group' ) ); 307 | } 308 | 309 | /** 310 | * Called before the mapping grouped field render begins. 311 | * 312 | * @since 0.1.0 313 | * 314 | * @param array $args Array of field arguments 315 | * @param CMB2_Field $field Field object. 316 | */ 317 | public function before_group( $args, CMB2_Field $field ) { 318 | // Do not get terms from parent post object 319 | add_filter( 'get_the_terms', array( __CLASS__, 'override_term_get' ), 9 ); 320 | 321 | // When the field starts rendering (now), store the current field object as property. 322 | self::$current_field = $field; 323 | 324 | // Check for stored 'before_group' parameter, and run that now. 325 | if ( $field->args( 'cmb2_group_map_before_group' ) ) { 326 | $field->peform_param_callback( 'cmb2_group_map_before_group' ); 327 | } 328 | 329 | do_action( 'cmb2_group_map_before_group', $field ); 330 | 331 | echo '
'; 332 | } 333 | 334 | /** 335 | * Called after the mapping grouped field render begins. 336 | * 337 | * @since 0.1.0 338 | * 339 | * @param array $args Array of field arguments 340 | * @param CMB2_Field $field Field object. 341 | */ 342 | public function after_group( $args, CMB2_Field $field ) { 343 | // Check for stored 'after_group' parameter, and run that now. 344 | if ( $field->args( 'cmb2_group_map_after_group' ) ) { 345 | $field->peform_param_callback( 'cmb2_group_map_after_group' ); 346 | } 347 | 348 | echo '
'; 349 | 350 | do_action( 'cmb2_group_map_after_group', $field ); 351 | 352 | // The field is now done rendering, so reset the current field property. 353 | self::$current_field = null; 354 | 355 | // Register our JS with the 'cmb2_script_dependencies' filter. 356 | add_filter( 'cmb2_script_dependencies', array( $this, 'register_js' ) ); 357 | } 358 | 359 | /** 360 | * Override term-gettting when these field groups are rendering. 361 | * 362 | * @since 0.1.0 363 | * 364 | * @param array $terms Array of term values 365 | * 366 | * @return array Array of term values (or empty array) 367 | */ 368 | public static function override_term_get( $terms ) { 369 | 370 | /* 371 | * If we're rendering the map group 372 | * AND Filter wasn't removed by CMB2_Group_Map_Get::override_term_get(), 373 | * It means we should return an empty array 374 | * (because there isn't an actual post, so it would pull from the host, 375 | * which is not correct) 376 | */ 377 | if ( self::is_rendering() ) { 378 | $terms = array(); 379 | } 380 | 381 | return $terms; 382 | } 383 | 384 | /** 385 | * Hooked into cmb2_script_dependencies, adds the mapped field style/js dependencies. 386 | * 387 | * @since 0.1.0 388 | * 389 | * @param array $dependencies Array of script dependencies 390 | * 391 | * @return array $dependencies Modified array of script dependencies 392 | */ 393 | public function register_js( $dependencies ) { 394 | $dependencies['cmb2_group_map'] = 'cmb2_group_map'; 395 | $assets_url = $this->get_url_from_dir( CMB2_GROUP_POST_MAP_DIR ) . 'lib/assets/'; 396 | 397 | wp_register_script( 398 | 'cmb2_group_map', 399 | $assets_url . 'js/cmb2-group-map.js', 400 | array( 'jquery', 'wp-backbone' ), 401 | self::VERSION, 402 | 1 403 | ); 404 | 405 | wp_localize_script( 'cmb2_group_map', 'CMB2Mapl10n', array( 406 | 'ajaxurl' => admin_url( 'admin-ajax.php', 'relative' ), 407 | 'strings' => self::$strings, 408 | ) ); 409 | 410 | wp_enqueue_style( 411 | 'cmb2_group_map', 412 | $assets_url . 'css/cmb2-group-map.css', 413 | array(), 414 | self::VERSION 415 | ); 416 | 417 | return $dependencies; 418 | } 419 | 420 | /** 421 | * Gets a URL from a provided directory. 422 | * 423 | * @since 0.1.0 424 | * 425 | * @param string $dir Directory path to convert. 426 | * 427 | * @return string Converted URL. 428 | */ 429 | public function get_url_from_dir( $dir ) { 430 | if ( 'WIN' === strtoupper( substr( PHP_OS, 0, 3 ) ) ) { 431 | // Windows 432 | $content_dir = str_replace( '/', DIRECTORY_SEPARATOR, WP_CONTENT_DIR ); 433 | $content_url = str_replace( $content_dir, WP_CONTENT_URL, $dir ); 434 | $url = str_replace( DIRECTORY_SEPARATOR, '/', $content_url ); 435 | 436 | } else { 437 | if ( false !== strpos( $dir, WP_CONTENT_DIR ) ) { 438 | $url = str_replace( 439 | array( WP_CONTENT_DIR, WP_PLUGIN_DIR ), 440 | array( WP_CONTENT_URL, WP_PLUGIN_URL ), 441 | $dir 442 | ); 443 | } else { 444 | // Check to see if it's in the root directory 445 | $to_trim = str_replace( ABSPATH, '', WP_CONTENT_DIR ); 446 | $url = str_replace( 447 | array( ABSPATH ), 448 | array( str_replace( $to_trim, '', WP_CONTENT_URL ) ), 449 | $dir 450 | ); 451 | } 452 | } 453 | 454 | return set_url_scheme( $url ); 455 | } 456 | 457 | /** 458 | * Get the primary object key for the object type. 459 | * 460 | * @since 0.1.0 461 | * 462 | * @param string $object_type Object type 463 | * 464 | * @return string Object ID key. 465 | */ 466 | public static function object_id_key( $object_type ) { 467 | switch ( $object_type ) { 468 | case 'comment': 469 | return 'comment_ID'; 470 | 471 | case 'term': 472 | return 'term_id'; 473 | 474 | case 'user': 475 | default: 476 | return 'ID'; 477 | } 478 | } 479 | 480 | /** 481 | * Hooks in the setting/getting overrides for this group field. 482 | * 483 | * @since 0.1.0 484 | * 485 | * @param string $field_id The group field ID. 486 | */ 487 | protected function hook_cmb2_overrides( $field_id ) { 488 | add_filter( "cmb2_override_{$field_id}_meta_save", array( $this, 'do_save' ), 10, 4 ); 489 | add_filter( "cmb2_override_{$field_id}_meta_value", array( $this, 'do_get' ), 10, 4 ); 490 | } 491 | 492 | /** 493 | * The save override 494 | * 495 | * @since 0.1.0 496 | * 497 | * @param [type] $override [description] 498 | * @param [type] $a [description] 499 | * @param [type] $args [description] 500 | * @param [type] $field_group [description] 501 | * 502 | * @return bool Returns true to shortcircuit CMB2 setting. 503 | */ 504 | public function do_save( $override, $a, $args, $field_group ) { 505 | require_once CMB2_GROUP_POST_MAP_DIR . 'lib/set.php'; 506 | $setter = new CMB2_Group_Map_Set( $field_group, $a['value'] ); 507 | $setter->save(); 508 | 509 | return true; // this shortcuts CMB2 save 510 | } 511 | 512 | /** 513 | * The get override 514 | * 515 | * @since 0.1.0 516 | * 517 | * @param [type] $nooverride [description] 518 | * @param [type] $object_id [description] 519 | * @param [type] $a [description] 520 | * @param [type] $field [description] 521 | * 522 | * @return mixed By not returning the passed-in value, we shortcircuit CMB2 getting. 523 | */ 524 | public function do_get( $nooverride, $object_id, $a, $field ) { 525 | remove_filter( "cmb2_override_{$a['field_id']}_meta_value", array( $this, 'do_get' ), 10, 4 ); 526 | 527 | require_once CMB2_GROUP_POST_MAP_DIR . 'lib/get.php'; 528 | $value = CMB2_Group_Map_Get::get_value( $field ); 529 | 530 | return $value; // this shortcuts CMB2 get 531 | } 532 | 533 | /** 534 | * Hooked into cmb2_group_map_updated, maps the updated/saved posts 535 | * to the original parent post. 536 | * 537 | * @since 0.1.0 538 | * 539 | * @param array $updated Array of updated IDs 540 | * @param int $original_object_id Parent id 541 | * @param CMB2_Field $field Group field object. 542 | */ 543 | public function map_to_original_object( $updated, $original_object_id, CMB2_Field $field ) { 544 | $updated = is_array( $updated ) ? $updated : array(); 545 | $object_ids = array(); 546 | 547 | foreach ( $updated as $object_id ) { 548 | if ( ! is_wp_error( $object_id ) ) { 549 | $object_ids[] = $object_id; 550 | } 551 | } 552 | 553 | if ( empty( $object_ids ) ) { 554 | self::delete_map_meta( $field ); 555 | } else { 556 | self::update_map_meta( $field, $object_ids, true ); 557 | } 558 | } 559 | 560 | /** 561 | * Hooked into cmb2_group_map_associated_object_deleted, updates the mapped objects 562 | * on the original parent post. 563 | * 564 | * @since 0.1.0 565 | * 566 | * @param array $deleted_id ID of the deleted associated object. 567 | * @param int $original_object_id Parent id 568 | * @param CMB2_Field $field Group field object. 569 | */ 570 | public function remove_from_original_object( $deleted_id, $original_object_id, CMB2_Field $field ) { 571 | 572 | if ( is_wp_error( $deleted_id ) ) { 573 | return; 574 | } 575 | 576 | self::remove_from_map_meta( $field, $deleted_id ); 577 | } 578 | 579 | /** 580 | * When deleting a post, we need to delete any mapped posts, if they exist. 581 | * If the group field has a 'sync_delete' param that is set to false, 582 | * this deletion sync will be disabled for that mapping. 583 | * 584 | * This can also be disabled with the 'cmb2_group_map_sync_delete' filter. 585 | * 586 | * @since 0.1.0 587 | * 588 | * @param int $post_id ID of the post being deleted. 589 | */ 590 | public function delete_associated_objects( $post_id ) { 591 | $post_type = get_post_type(); 592 | 593 | foreach ( $this->group_fields as $cmb_id => $fields ) { 594 | foreach ( $fields as $field_id => $field ) { 595 | 596 | // Only sync for 'post' object type. 597 | if ( 'post' !== $field['object_type_map'] ) { 598 | continue; 599 | } 600 | 601 | $types = $field['original_object_types']; 602 | $types = is_array( $types ) ? $types : array( $types ); 603 | 604 | // Not the field we're looking for. 605 | if ( ! in_array( $post_type, $types, 1 ) ) { 606 | continue; 607 | } 608 | 609 | // If $field['sync_delete'] is false, then do not sync deletion. 610 | if ( isset( $field['sync_delete'] ) && ! $field['sync_delete'] ) { 611 | continue; 612 | } 613 | 614 | // If 'cmb2_group_map_sync_delete' filter value is false, then do not sync deletion. 615 | if ( ! apply_filters( 'cmb2_group_map_sync_delete', true, $post_id, $field ) ) { 616 | continue; 617 | } 618 | 619 | $object_ids = get_post_meta( $post_id, $field['id'], 1 ); 620 | 621 | // If no connected posts to delete. 622 | if ( ! is_array( $object_ids ) || empty( $object_ids ) ) { 623 | continue; 624 | } 625 | 626 | // Ok, delete them. 627 | foreach ( $object_ids as $id ) { 628 | wp_delete_post( $id, 1 ); 629 | } 630 | } 631 | } 632 | } 633 | 634 | /** 635 | * Ajax handler for getting group data. 636 | * 637 | * @since 0.1.0 638 | */ 639 | public function get_ajax_input_data() { 640 | require_once CMB2_GROUP_POST_MAP_DIR . 'lib/ajax.php'; 641 | 642 | try { 643 | $ajax_handler = new CMB2_Group_Map_Ajax( $_POST, $this->group_fields ); 644 | $ajax_handler->send_input_data(); 645 | } catch ( Exception $e ) { 646 | wp_send_json_error( $e->getMessage() ); 647 | } 648 | } 649 | 650 | /** 651 | * Ajax handler for deleting associated posts. 652 | * 653 | * @since 0.1.0 654 | */ 655 | public function ajax_delete_item() { 656 | require_once CMB2_GROUP_POST_MAP_DIR . 'lib/ajax.php'; 657 | 658 | try { 659 | $ajax_handler = new CMB2_Group_Map_Ajax( $_POST, $this->group_fields ); 660 | $ajax_handler->delete(); 661 | } catch ( Exception $e ) { 662 | wp_send_json_error( $e->getMessage() ); 663 | } 664 | } 665 | 666 | /** 667 | * Get the object ids value for a mapped group field. 668 | * 669 | * @since 0.1.0 670 | * 671 | * @param array|CMB2_Field $group_field Either CMB2_Field group object, or array. 672 | * 673 | * @return mixed Result of get. 674 | */ 675 | public static function get_map_meta( $group_field ) { 676 | $args = self::parse_group_field_args( $group_field ); 677 | 678 | if ( ! $args ) { 679 | return false; 680 | } 681 | 682 | $object_ids = get_metadata( $args['object_type'], $args['object_id'], $args['id'], $args['single'] ); 683 | 684 | return is_array( $object_ids ) ? $object_ids : array(); 685 | } 686 | 687 | /** 688 | * Updates the object ids value for a mapped group field. 689 | * 690 | * @since 0.1.0 691 | * 692 | * @param array|CMB2_Field $group_field Either CMB2_Field group object, or array. 693 | * 694 | * @return mixed Result of update. 695 | */ 696 | public static function update_map_meta( $group_field, array $value, $replace = false ) { 697 | $args = self::parse_group_field_args( $group_field ); 698 | 699 | if ( ! $args ) { 700 | return false; 701 | } 702 | 703 | if ( ! $replace ) { 704 | $existing = self::get_map_meta( $args ); 705 | 706 | if ( ! is_array( $existing ) ) { 707 | return false; 708 | } 709 | 710 | $value = array_merge( $value, $existing ); 711 | $value = array_unique( $value ); 712 | } 713 | 714 | return update_metadata( $args['object_type'], $args['object_id'], $args['id'], $value ); 715 | } 716 | 717 | /** 718 | * Deletes the object ids value for a mapped group field. 719 | * 720 | * @since 0.1.0 721 | * 722 | * @param array|CMB2_Field $group_field Either CMB2_Field group object, or array. 723 | * 724 | * @return mixed Result of delete. 725 | */ 726 | public static function delete_map_meta( $group_field ) { 727 | $args = self::parse_group_field_args( $group_field ); 728 | 729 | if ( ! $args ) { 730 | return false; 731 | } 732 | 733 | return delete_metadata( $args['object_type'], $args['object_id'], $args['id'] ); 734 | } 735 | 736 | /** 737 | * Removes object id(s) from value for a mapped group field. 738 | * 739 | * @since 0.1.0 740 | * 741 | * @param array|CMB2_Field $group_field Either CMB2_Field group object, or array. 742 | * 743 | * @return mixed Result of removal. 744 | */ 745 | public static function remove_from_map_meta( $group_field, $remove ) { 746 | $args = self::parse_group_field_args( $group_field ); 747 | 748 | if ( ! $args ) { 749 | return false; 750 | } 751 | 752 | $existing = self::get_map_meta( $args ); 753 | 754 | // Nothing to remove. 755 | if ( ! $existing || empty( $existing ) ) { 756 | return false; 757 | } 758 | 759 | $remove = is_array( $remove ) ? $remove : array( $remove ); 760 | $removed = array(); 761 | 762 | foreach ( $remove as $id ) { 763 | if ( in_array( $id, $existing ) ) { 764 | // Search 765 | $pos = array_search( $id, $existing ); 766 | // Remove from array 767 | unset( $existing[ $pos ] ); 768 | 769 | $removed[] = $id; 770 | } 771 | } 772 | 773 | // Nothing was removed, so don't proceed. 774 | if ( empty( $removed ) ) { 775 | return $removed; 776 | } 777 | 778 | // Ok, resave the meta value w/ the removals complete. 779 | if ( empty( $existing ) ) { 780 | // if it's now empty, just delete it. 781 | $result = self::delete_map_meta( $args ); 782 | } else { 783 | $result = self::update_map_meta( $args, $existing, true ); 784 | } 785 | 786 | // If update was complete, send back the array of the leftover IDs. 787 | return $result ? $existing : false; 788 | } 789 | 790 | /** 791 | * Gets the object_type, object_id, and field id (meta key) parameters for 792 | * get/update/delete_metadata function calls. 793 | * 794 | * Can be provided a CMB2_Field object or an array. 795 | * 796 | * @since 0.1.0 797 | * 798 | * @param array|CMB2_Field $group_field Either CMB2_Field group object, or array. 799 | * 800 | * @return array|false Array of args, or false if 'id' was not found. 801 | */ 802 | protected static function parse_group_field_args( $group_field ) { 803 | if ( is_a( $group_field, 'CMB2_Field' ) ) { 804 | $args['object_type'] = $group_field->args( 'original_object_type' ) 805 | ? $group_field->args( 'original_object_type' ) 806 | : $group_field->object_type; 807 | $args['object_id'] = $group_field->object_id; 808 | $args['id'] = $group_field->id(); 809 | $args['single'] = true; 810 | 811 | } else { 812 | 813 | $args = wp_parse_args( $group_field, array( 814 | 'object_type' => 'post', 815 | 'object_id' => get_the_id(), 816 | 'id' => '', 817 | 'single' => true, 818 | ) ); 819 | 820 | } 821 | 822 | if ( ! $args['id'] ) { 823 | return false; 824 | } 825 | 826 | return $args; 827 | } 828 | 829 | /** 830 | * Helper method to determine if Lib is in the middle of 831 | * rendering a group mapping field. 832 | * 833 | * @since 0.1.0 834 | * 835 | * @return boolean True if rendering. 836 | */ 837 | public static function is_rendering() { 838 | return (bool) self::$current_field; 839 | } 840 | 841 | /** 842 | * Return current field. 843 | * 844 | * @since 0.1.0 845 | * 846 | * @return CMB2_Field|null If in the middle of rendering, will be the current group field object. 847 | */ 848 | public static function get_current_field() { 849 | return self::$current_field; 850 | } 851 | 852 | /** 853 | * Magic getter for our object. 854 | * 855 | * @since 0.1.0 856 | * 857 | * @param string $field 858 | * @throws Exception Throws an exception if the field is invalid. 859 | * 860 | * @return mixed 861 | */ 862 | public function __get( $field ) { 863 | switch ( $field ) { 864 | case 'group_fields': 865 | return $this->{$field}; 866 | default: 867 | throw new Exception( 'Invalid ' . __CLASS__ . ' property: ' . $field ); 868 | } 869 | } 870 | 871 | } 872 | CMB2_Group_Map::get_instance(); 873 | --------------------------------------------------------------------------------