├── .github └── CODEOWNERS ├── .gitignore ├── README.md ├── acf-FIELD-NAME ├── assets │ ├── css │ │ └── field.css │ ├── images │ │ └── field-preview-custom.png │ └── js │ │ └── field.js ├── class-PREFIX-acf-field-FIELD-NAME.php └── init.php ├── package-lock.json ├── package.json └── rename.js /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @AdvancedCustomFields/acf-eng -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # System files 2 | .DS_Store 3 | ehthumbs.db 4 | Thumbs.db 5 | 6 | # IDE config 7 | .idea 8 | .vscode 9 | 10 | # Dependencies 11 | node_modules 12 | vendor 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ACF Example Field Type 2 | 3 | Reference code to register a new [Advanced Custom Fields](https://www.advancedcustomfields.com/) field type. 4 | 5 | Intended for use in an existing theme or plugin. 6 | 7 | ## Setup 8 | 9 | 1. Download this repository: 10 | - [Download as a ZIP file](https://github.com/advancedcustomfields/acf-example-field-type/archive/refs/heads/main.zip) 11 | - Or clone with `git clone https://github.com/advancedcustomfields/acf-example-field-type.git` 12 | 2. Rename placeholder strings in file names and content such as `FIELD_NAME` manually or with the helper script: 13 | - `cd` into the directory and run `npm ci` (requires [Node.js](https://nodejs.org/)). 14 | - Run `npm run rename` and follow the prompts. 15 | 3. Copy the renamed `acf-FIELD-NAME` directory into your theme or plugin. 16 | 4. Add this code to your theme or plugin to initialize the new field type, replacing 'acf-FIELD-NAME' with the new folder name: 17 | 18 | ```php 19 | include_once __DIR__ . '/acf-FIELD-NAME/init.php'; 20 | ``` 21 | 22 | You should now see your new field type in the “Field Type” list when you add a new field. 23 | 24 | ## Customization 25 | 26 | - Change your field type's settings and behavior by editing `class-PREFIX-acf-field-FIELD-NAME.php`. 27 | - Edit or remove `field.css` to control the field's appearance when edited by publishers (e.g. on post editor screens). 28 | - Edit or remove `field.js` to adjust the field's behavior when edited by publishers. 29 | 30 | If you remove `field.css` or `field.js`, be sure to also remove the `wp_enqueue` calls in `class-PREFIX-acf-field-FIELD-NAME.php`. 31 | 32 | If you plan to use the CSS and JS files, update the '1.0' version number in `class-PREFIX-acf-field-FIELD-NAME.php` to use your theme or plugin version number or constant. This helps to invalidate browser caches for your field type assets when you update your theme or plugin. 33 | 34 | Refer to the [acf_field class](https://github.com/AdvancedCustomFields/acf/blob/master/includes/fields/class-acf-field.php) for information about available methods to override. 35 | -------------------------------------------------------------------------------- /acf-FIELD-NAME/assets/css/field.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Included when fields of this type are rendered for editing by publishers. 3 | */ 4 | .acf-field-FIELD-NAME .setting-font-size { 5 | color: green; 6 | } 7 | -------------------------------------------------------------------------------- /acf-FIELD-NAME/assets/images/field-preview-custom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdvancedCustomFields/acf-example-field-type/12391e59f5a188787c0800768c7f4c6cbf70a676/acf-FIELD-NAME/assets/images/field-preview-custom.png -------------------------------------------------------------------------------- /acf-FIELD-NAME/assets/js/field.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Included when FIELD_NAME fields are rendered for editing by publishers. 3 | */ 4 | ( function( $ ) { 5 | function initialize_field( $field ) { 6 | /** 7 | * $field is a jQuery object wrapping field elements in the editor. 8 | */ 9 | console.log( 'FIELD_NAME field initialized' ); 10 | } 11 | 12 | if( typeof acf.add_action !== 'undefined' ) { 13 | /** 14 | * Run initialize_field when existing fields of this type load, 15 | * or when new fields are appended via repeaters or similar. 16 | */ 17 | acf.add_action( 'ready_field/type=FIELD_NAME', initialize_field ); 18 | acf.add_action( 'append_field/type=FIELD_NAME', initialize_field ); 19 | } 20 | } )( jQuery ); 21 | -------------------------------------------------------------------------------- /acf-FIELD-NAME/class-PREFIX-acf-field-FIELD-NAME.php: -------------------------------------------------------------------------------- 1 | name = 'FIELD_NAME'; 38 | 39 | /** 40 | * Field type label. 41 | * 42 | * For public-facing UI. May contain spaces. 43 | */ 44 | $this->label = __( 'FIELD_LABEL', 'TEXTDOMAIN' ); 45 | 46 | /** 47 | * The category the field appears within in the field type picker. 48 | */ 49 | $this->category = 'basic'; // basic | content | choice | relational | jquery | layout | CUSTOM GROUP NAME 50 | 51 | /** 52 | * Field type Description. 53 | * 54 | * For field descriptions. May contain spaces. 55 | */ 56 | $this->description = __( 'FIELD_DESCRIPTION', 'TEXTDOMAIN' ); 57 | 58 | /** 59 | * Field type Doc URL. 60 | * 61 | * For linking to a documentation page. Displayed in the field picker modal. 62 | */ 63 | $this->doc_url = 'FIELD_DOC_URL'; 64 | 65 | /** 66 | * Field type Tutorial URL. 67 | * 68 | * For linking to a tutorial resource. Displayed in the field picker modal. 69 | */ 70 | $this->tutorial_url = 'FIELD_TUTORIAL_URL'; 71 | 72 | /** 73 | * Defaults for your custom user-facing settings for this field type. 74 | */ 75 | $this->defaults = array( 76 | 'font_size' => 14, 77 | ); 78 | 79 | /** 80 | * Strings used in JavaScript code. 81 | * 82 | * Allows JS strings to be translated in PHP and loaded in JS via: 83 | * 84 | * ```js 85 | * const errorMessage = acf._e("FIELD_NAME", "error"); 86 | * ``` 87 | */ 88 | $this->l10n = array( 89 | 'error' => __( 'Error! Please enter a higher value', 'TEXTDOMAIN' ), 90 | ); 91 | 92 | $this->env = array( 93 | 'url' => site_url( str_replace( ABSPATH, '', __DIR__ ) ), // URL to the acf-FIELD-NAME directory. 94 | 'version' => '1.0', // Replace this with your theme or plugin version constant. 95 | ); 96 | 97 | /** 98 | * Field type preview image. 99 | * 100 | * A preview image for the field type in the picker modal. 101 | */ 102 | $this->preview_image = $this->env['url'] . '/assets/images/field-preview-custom.png'; 103 | 104 | parent::__construct(); 105 | } 106 | 107 | /** 108 | * Settings to display when users configure a field of this type. 109 | * 110 | * These settings appear on the ACF “Edit Field Group” admin page when 111 | * setting up the field. 112 | * 113 | * @param array $field 114 | * @return void 115 | */ 116 | public function render_field_settings( $field ) { 117 | /* 118 | * Repeat for each setting you wish to display for this field type. 119 | */ 120 | acf_render_field_setting( 121 | $field, 122 | array( 123 | 'label' => __( 'Font Size','TEXTDOMAIN' ), 124 | 'instructions' => __( 'Customise the input font size','TEXTDOMAIN' ), 125 | 'type' => 'number', 126 | 'name' => 'font_size', 127 | 'append' => 'px', 128 | ) 129 | ); 130 | 131 | // To render field settings on other tabs in ACF 6.0+: 132 | // https://www.advancedcustomfields.com/resources/adding-custom-settings-fields/#moving-field-setting 133 | } 134 | 135 | /** 136 | * HTML content to show when a publisher edits the field on the edit screen. 137 | * 138 | * @param array $field The field settings and values. 139 | * @return void 140 | */ 141 | public function render_field( $field ) { 142 | // Debug output to show what field data is available. 143 | echo '
';
144 | 		print_r( $field );
145 | 		echo '
'; 146 | 147 | // Display an input field that uses the 'font_size' setting. 148 | ?> 149 | 156 | env['url'] ); 168 | $version = $this->env['version']; 169 | 170 | wp_register_script( 171 | 'PREFIX-FIELD-NAME', 172 | "{$url}assets/js/field.js", 173 | array( 'acf-input' ), 174 | $version 175 | ); 176 | 177 | wp_register_style( 178 | 'PREFIX-FIELD-NAME', 179 | "{$url}assets/css/field.css", 180 | array( 'acf-input' ), 181 | $version 182 | ); 183 | 184 | wp_enqueue_script( 'PREFIX-FIELD-NAME' ); 185 | wp_enqueue_style( 'PREFIX-FIELD-NAME' ); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /acf-FIELD-NAME/init.php: -------------------------------------------------------------------------------- 1 | =6" 22 | } 23 | }, 24 | "node_modules/prompts": { 25 | "version": "2.4.2", 26 | "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", 27 | "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", 28 | "dev": true, 29 | "dependencies": { 30 | "kleur": "^3.0.3", 31 | "sisteransi": "^1.0.5" 32 | }, 33 | "engines": { 34 | "node": ">= 6" 35 | } 36 | }, 37 | "node_modules/sisteransi": { 38 | "version": "1.0.5", 39 | "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", 40 | "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", 41 | "dev": true 42 | } 43 | }, 44 | "dependencies": { 45 | "kleur": { 46 | "version": "3.0.3", 47 | "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", 48 | "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", 49 | "dev": true 50 | }, 51 | "prompts": { 52 | "version": "2.4.2", 53 | "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", 54 | "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", 55 | "dev": true, 56 | "requires": { 57 | "kleur": "^3.0.3", 58 | "sisteransi": "^1.0.5" 59 | } 60 | }, 61 | "sisteransi": { 62 | "version": "1.0.5", 63 | "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", 64 | "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", 65 | "dev": true 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wpengine/acf-example-field-type", 3 | "version": "1.0.0", 4 | "description": "Reference code to register a new Advanced Custom Fields field type.", 5 | "main": "index.js", 6 | "scripts": { 7 | "rename": "node rename.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/deliciousbrains/acf-example-field-type.git" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "bugs": { 17 | "url": "https://github.com/deliciousbrains/acf-example-field-type/issues" 18 | }, 19 | "homepage": "https://github.com/deliciousbrains/acf-example-field-type#readme", 20 | "devDependencies": { 21 | "prompts": "^2.4.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /rename.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Rename script. Run with `npm ci && npm run rename`. 3 | * 4 | * Run before copying the acf-FIELD-NAME folder to your theme or plugin so that 5 | * you don't have to replace placeholder strings like `FIELD-NAME` manually. 6 | * 7 | * This script should not be copied to your theme or plugin. 8 | */ 9 | const prompts = require("prompts"); 10 | const fs = require("fs"); 11 | const path = require("path"); 12 | 13 | const questions = [ 14 | { 15 | type: "text", 16 | name: "fieldLabel", 17 | message: "Field label:", 18 | initial: "Amazing Field", 19 | validate: (text) => 20 | text.match(/[^A-Za-z0-9_-\s]/g) 21 | ? "Label allows alphanumeric English characters, spaces, underscores and dashes." 22 | : true, 23 | }, 24 | { 25 | type: "text", 26 | name: "prefix", 27 | message: "Function prefix:", 28 | initial: "company_or_project_name", 29 | validate: (text) => 30 | text.match(/[^a-z0-9_]/g) 31 | ? "Prefix allows lowercase alphanumeric English characters and underscores." 32 | : true, 33 | }, 34 | { 35 | type: "text", 36 | name: "textDomain", 37 | message: "Text domain:", 38 | initial: "plugin-or-theme-name", 39 | validate: (text) => 40 | text.match(/[^a-z0-9-]/g) ? "Text domain allows a-z, 0-9 and '-'." : true, 41 | }, 42 | { 43 | type: "text", 44 | name: "fieldDescription", 45 | message: "Field Description:", 46 | initial: "This field does amazing things.", 47 | }, 48 | { 49 | type: "text", 50 | name: "fieldDocLink", 51 | message: "Documentation Link:", 52 | initial: "", 53 | }, 54 | { 55 | type: "text", 56 | name: "fieldTutorialLink", 57 | message: "Field Tutorial Link:", 58 | initial: "", 59 | }, 60 | ]; 61 | 62 | (async () => { 63 | const dir = path.resolve(__dirname, "acf-FIELD-NAME"); 64 | if (!fs.existsSync(dir)) { 65 | console.error("Could not find the acf-FIELD-NAME folder."); 66 | console.error("You can ony rename a fresh copy of the repo."); 67 | process.exit(1); 68 | } 69 | 70 | const response = await prompts(questions); 71 | 72 | if (!response.fieldLabel || !response.prefix || !response.textDomain) { 73 | console.error("You must provide all placeholder string replacements."); 74 | process.exit(1); 75 | } 76 | 77 | const replacements = { 78 | "class-PREFIX": "class-" + response.prefix.replace(/_/g, "-"), 79 | PREFIX: response.prefix, 80 | FIELD_LABEL: response.fieldLabel, 81 | FIELD_NAME: response.fieldLabel.toLowerCase().replace(/[-\s]/g, "_"), 82 | "FIELD-NAME": response.fieldLabel.toLowerCase().replace(/[_\s]/g, "-"), 83 | TEXTDOMAIN: response.textDomain, 84 | FIELD_DESCRIPTION: response.fieldDescription, 85 | FIELD_DOC_URL: response.fieldDocLink, 86 | FIELD_TUTORIAL_URL: response.fieldTutorialLink, 87 | }; 88 | 89 | console.log("Replacing in file names…"); 90 | renameFiles(replacements); 91 | 92 | console.log("Replacing in file contents…"); 93 | replaceStrings(replacements); 94 | 95 | console.log("Done."); 96 | })(); 97 | 98 | /** 99 | * Renames files and folders in the current directory. 100 | * 101 | * @param {object} replacements Replace keys with their values. 102 | */ 103 | const renameFiles = (replacements = {}) => { 104 | const dir = path.resolve(__dirname); 105 | 106 | /** 107 | * Rename acf-FIELD-NAME directory first. Prevents write failure during file 108 | * renames due to parent directory being renamed by the first find-replace. 109 | */ 110 | fs.renameSync( 111 | path.resolve(__dirname, "acf-FIELD-NAME"), 112 | path.resolve( 113 | __dirname, 114 | "acf-FIELD-NAME".replace(/FIELD-NAME/g, replacements["FIELD-NAME"]) 115 | ) 116 | ); 117 | 118 | const files = walkSync(dir); 119 | 120 | files.forEach((file) => { 121 | let newPath = Object.keys(replacements).reduce( 122 | (acc, key) => acc.replace(new RegExp(key, "g"), replacements[key]), 123 | file 124 | ); 125 | 126 | fs.renameSync(file, newPath); 127 | }); 128 | }; 129 | 130 | const replaceStrings = (replacements = {}) => { 131 | const dir = path.resolve(__dirname); 132 | const files = walkSync(dir); 133 | 134 | files.forEach((file) => { 135 | const oldContent = fs.readFileSync(file, "utf8"); 136 | 137 | let newFileContent = Object.keys(replacements).reduce( 138 | (acc, key) => acc.replace(new RegExp(key, "g"), replacements[key]), 139 | oldContent 140 | ); 141 | 142 | fs.writeFileSync(file, newFileContent, "utf8"); 143 | }); 144 | }; 145 | 146 | /** 147 | * Gets all files in `dir` excluding repo paths such as node_modules. 148 | * 149 | * @param {string} dir 150 | * @param {array} filelist List of existing files to append to. 151 | * @returns {array} List of files. 152 | */ 153 | const walkSync = (dir, filelist = []) => { 154 | fs.readdirSync(dir).forEach((file) => { 155 | filelist = fs.statSync(path.join(dir, file)).isDirectory() 156 | ? walkSync(path.join(dir, file), filelist) 157 | : filelist.concat(path.join(dir, file)); 158 | }); 159 | return filelist.filter( 160 | (path) => !path.match(/node_modules|\.git|package-lock|rename\.js/) 161 | ); 162 | }; 163 | --------------------------------------------------------------------------------