=== Nik Custom Fields ===
Contributors: dimon1873
Tags: custom fields, meta box, repeater, flexible content, options page
Requires at least: 6.1
Tested up to: 7.0
Stable tag: 2.0.5
Requires PHP: 7.4
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Custom fields for WordPress: field groups, repeaters, flexible content, blocks and a familiar template API.

== Description ==

Create field groups, attach them to posts, pages, products, users, taxonomy
terms, comments or a site-wide options page, and read the values back with the
functions you already know.

Everything is included. There is no paid tier, no key to enter and no feature
held back, and the plugin makes no external requests at all — nothing about your
site is sent anywhere.

**30 field types**

Text, Textarea, Number, Range, Email, URL, Password, Rich text, Image, File,
Gallery, Select, Checkbox, Radio, Button group, True/False, Link, Post object,
Relationship, Taxonomy, User, Colour, Date, Date and time, Time, Message, Tab,
Repeater, Flexible content, Clone.

**A template API you already know**

`nik_get_field()`, `nik_the_field()`, `nik_have_rows()`, `nik_the_row()`,
`nik_get_sub_field()`, `nik_update_field()`, `nik_get_row_layout()` and the rest.
This is the supported API and it is always available.

For a site being moved off another custom-fields plugin there is a
compatibility layer that also defines the unprefixed names those themes call —
`get_field()`, `have_rows()` and about thirty more — so templates keep working
unchanged. It is **off by default** and lives under Settings, because those
names belong to the global namespace; it never activates while another plugin
already defines them.

**Repeater rows are queryable**

Every value inside a repeater or flexible-content field is mirrored into a flat
meta key, so `meta_query` and `orderby` can reach inside a row:

`array( 'key' => nik_meta_key( 'specs', '0_name' ), 'value' => 'weight' )`

**Other things it does**

* Conditional logic — show a field only while another holds a certain value.
* Nesting to any depth: a repeater inside a flexible layout inside a repeater.
* Import and export a whole configuration as JSON, matched by key so ids may
  differ between dev, staging and production.
* Field groups can be declared in PHP or read from a `nik-cf-json` directory in
  the theme, so the configuration can travel in version control.
* Optional REST exposure per field group, with values already in their
  configured return format.
* Renaming a field carries its stored values across instead of orphaning them.
* Takes over from an existing custom-fields setup, including one whose plugin
  has already been removed and left only values behind.
* Front-end forms, blocks built from a field group, and three shortcodes.
* Ukrainian and Russian translations included, following the admin's language.

**Documentation and demo**

* Documentation: https://products.nikdimon.com/nik-custom-fields-docs/
* Live demo, every field type on one page: https://products.nikdimon.com/nik-custom-fields-demo/
* The author's other WordPress plugins: https://products.nikdimon.com/

== Installation ==

1. Install through **Plugins → Add New**, or upload the folder to
   `/wp-content/plugins/nik-custom-fields/`.
2. Activate the plugin.
3. Go to **Nik Custom Fields → Field Groups** and create your first group.

A new install starts empty: no sample groups, no demo fields, nothing to delete
before you begin. If the site already has custom-fields data from another
plugin, **Nik Custom Fields → Tools** offers to bring it across, and says so in
an admin notice when it finds any.

== Requirements ==

* WordPress 6.1 or newer
* PHP 7.4 or newer
* **MySQL 5.7+ or MariaDB 10.2+** — the schema uses JSON columns. Activation is
  refused on an older server rather than creating tables that cannot hold the
  data.

== Frequently Asked Questions ==

= Is it really free? =

Yes. The whole plugin is GPL, every field type is included, and there is no
upsell inside the editing screens. The author's other plugins are listed on one
"About" tab and nowhere else.

= Does it send anything to an external server? =

No. There are no licence checks, no telemetry and no remote calls of any kind.

= Does it conflict with another custom-fields plugin? =

No. The plugin's own API is prefixed `nik_*` and collides with nothing. The
compatibility layer that defines the unprefixed names is off unless you switch
it on, and even then it never redeclares a name another plugin has already
taken, and never runs while ACF is installed.

= Does a fresh install create any fields of its own? =

No. Activation creates three database tables and a settings row, and nothing
else. Every field group on the site is one you or an import made.

= Can I move field groups between sites? =

Yes, under Tools. Groups are matched on their key rather than their numeric id,
so ids may differ between installs. Import only ever changes configuration; the
values entered in posts are never touched.

= What happens to my data if I delete the plugin? =

Deleting it through the Plugins screen drops the plugin's own tables and its
field values, because that is what "delete" is asked to mean. Deactivating
changes nothing at all, so it is the safe way to switch it off temporarily.

== Screenshots ==

1. The field group list.
2. Building a field group: 30 types, conditional logic, per-field width.
3. A repeater with nested subfields on the post edit screen.
4. Tools: import, export, and taking over from an existing custom-fields setup.

== Shortcodes ==

* `[nik_cf_field field="key"]` — one field's value.
* `[nik_cf_group group="group-key"]` — every field in a group, as a table.
* `[nik_cf_gallery field="key" size="medium"]` — a gallery field.

All three accept `post_id`, which also takes `option`, `user_5` or `term_12`.

== Changelog ==

= 2.0.5 =
* The example in the migration code that explains how the old plugin stored its data now uses obviously invented identifiers. It had been written against a real site, and three of that site's field keys were sitting in the comment.

= 2.0.4 =
* Fixed: a WYSIWYG field on a tab could not be typed into at all. Tabs are built after the fields render, and that move puts every panel but the first out of sight — an editor created before the move ends up in a hidden panel, where TinyMCE has nothing to measure: the iframe stays zero-sized, initialisation never finishes and the body never gets `contenteditable`. Editors are now created once their tab is open, and the same applies to a collapsed repeater row.

= 2.0.3 =
* Fixed: everything typed into a WYSIWYG field was dropped on save. The content is copied back to the posting textarea by TinyMCE's own triggerSave(), and that call does nothing unless TinyMCE has set its internal `initialized` flag — which an editor created by wp.editor.initialize() does not always get, although it renders and edits perfectly. The field simply reappeared with its previous text. The content is now copied across directly, and an editor left on the Text tab is skipped so its textarea stays the original.

= 2.0.2 =
* Fixed: a File field returned nothing for anything that was not an image. The value was formatted through the image lookup, which has no answer for a PDF, a video or an archive, so get_field() handed back null and the file looked as though it had never been chosen. Non-images now fall back to the attachment URL.

= 2.0.1 =
* Fixed: a field setting typed into the builder came back empty after saving. Every field type keeps its settings block in the page, and they share input names — eighteen types offer a "placeholder" — so the last hidden empty copy was overwriting whatever had been typed into the visible one. Only the block on screen is submitted now. Placeholders, default values, minimum, maximum, step, CSS class and the rest all stay put.
* Added: a Description for every field, shown under it to whoever fills it in. The column and the rendering had been there from the start; there was simply nowhere to type it, so every save wrote an empty string back. Subfields inside repeaters and flexible layouts get one too.
* Fixed: number inputs and text areas in the field builder were left unstyled, so they did not match the boxes beside them.

= 2.0.0 =
* The Ukrainian and Russian translations ship with the plugin again, so the interface is in the administrator's language the moment it is installed, from wherever it was installed. A language pack from translate.wordpress.org still takes priority when one exists, so contributed translations override the bundled ones rather than fighting them.
* Fixed: a locale change made during the request — what a multilingual plugin or an email in another language does — left this plugin's own strings in the previous language.

= 1.9.9 =
* Changed: the compatibility layer that defines `get_field()`, `have_rows()` and about thirty more unprefixed names is now off unless you switch it on under Settings. Those names belong to the global namespace, and claiming them is a service to a site being migrated rather than something a fresh install should do on its own. Sites that already relied on it keep it: an install from before this version is left exactly as it was.
* The prefixed `nik_*` API is unchanged and, as always, available whatever that setting says.

= 1.9.8 =
* Changed: the Documentation tab is gone. The documentation lives on the plugin's site, where it can be kept current and shown running, so the menu entry opens it in a new tab instead of carrying a copy that drifts from it.
* Changed: the field-group builder's script is a real file with its data handed to it, rather than markup printed with PHP interpolated into it.
* Changed: shortcode output is filtered through wp_kses_post() before it reaches the page. Rich-text and embed fields still render as HTML; scripts and event handlers in a stored value no longer do.
* Changed: only the translation template ships in the package. Ukrainian and Russian move to translate.wordpress.org, where WordPress builds language packs from them and installs them automatically — a bundled catalogue would compete with those.
* Changed: every link points at products.nikdimon.com. The bare domain answers TLS with a certificate for another host, which is a browser warning for anyone who clicks.

= 1.9.7 =
* Fixed: a field group attached to nothing appeared on everything — every edit screen and every options page — instead of nowhere. An empty rule set means "there is nothing to fail" to the rule evaluator, which was then read as "this group applies here". A group with no placement is now placed nowhere, and the field group list marks it **Not placed** so it is a statement rather than a mystery. A group used only as a clone source is meant to look like that, and a clone still reaches it.
* Fixed: switching the rich-text field to the Text tab collapsed it to a narrow empty box. The editor is attached from JavaScript, which wires up TinyMCE and the quicktags but adds no classes, so the textarea never carried the one the WordPress stylesheet targets — the code view fell back to a 175-pixel browser default. It is now full width, and the leftover styles from the editor this replaced, one of which hid the textarea outright, are gone.

= 1.9.6 =
* Fixed: update_field() refused to write a row that contained a checkbox or a multi-select. Such a value is stored as a JSON list, and code that reads a row and writes it back hands that string in unchanged — validation compared the whole string against the options, never matched, and the write was rejected with nothing but a `false` to show for it.

= 1.9.5 =
* Fixed: a field group placed on one page appeared on every screen in the site, the options pages included. A group declared in code arrives with an empty stored rule set, and an empty set was read as "this group has no placement" — which means it applies everywhere — instead of falling through to the page, template and term it was actually given. The same lists were also dropped whenever no post type was named alongside them.
* Fixed: an options page declared by a theme could not be removed. The call runs on every request, and each one wrote a database row, so deleting the page from the screen removed a row that the next page load put straight back — it read as a row that would not go away. Pages declared in code now live in code: they are listed as "In code", carry no Edit or Delete button, and the note says which call to remove. Any stored row a declaration has taken over is cleared away once. Values are keyed by the page key and are untouched.
* Fixed: the Edit button on an options page opened an empty "Add" form. The row id arrives from the database as a string and was compared strictly against a number, so the match never happened and nothing was ever pre-filled.
* Changed: field groups created by taking a site over from another custom-fields plugin are keyed `imported-…` rather than with that plugin's name. Groups written under the old prefix are still recognised and are renamed in place on the next import.
* Tests: 250 assertions. The new ones cover the code-declared options page end to end, the Edit lookup, and both key prefixes.

= 1.9.4 =
* Fixed: repeater and flexible-content rows could not be dragged at all. The sort handle was declared relative to the row list, while jQuery UI looks for it inside the row it is about to move, so no handle ever matched and the drag was refused before it began.
* Fixed: the drag grip was invisible. It relied on the `dashicons-menu` glyph, which is empty in current WordPress, leaving a blank 20×20 box where the grip should be — the icon is now drawn in CSS and depends on nothing outside the plugin.

= 1.9.3 =
* Fixed: reading a value and writing it back destroyed it. get_field() returns a formatted value — an image as an array, a post as WP_Post, a term as WP_Term — and passing that straight to update_field() emptied media fields (the array became 0) and pointed relations at post 1 (PHP casts any non-empty array to 1). Taxonomy and relationship values were dropped outright, and a term object made the save die with a fatal error. Media and relational fields now accept ids, formatted arrays, objects and attachment URLs alike, so read-modify-write of a whole row is lossless.
* Tests: the release gate now writes back everything it reads for every structural field type, which is how the above was found.

= 1.9.2 =
* Fixed: a link field refused to let the post save at all. It was rendered as a browser URL box, which only accepts an absolute address, so an anchor ("#reviews") or a site-relative path ("/shop/") counted as invalid — and when that field sat inside a collapsed repeater row or was hidden by conditional logic, the browser could not focus what it objected to, so "Update" did nothing and explained nothing. Link fields are now plain text boxes with a URL keyboard hint; addresses are still sanitized on the way into storage.

= 1.9.1 =
* Fixed: inside a repeater or a flexible layout, a subfield was rendered without its own configuration. A post picker lost "allow multiple" and replaced the previous choice instead of adding to it; it also lost its post-type restriction and offered every post type on the site. A term picker lost its taxonomy, and number inputs lost their min and max. Only the editing screen was affected — stored values were always read back correctly.

= 1.9.0 =
* The plugin is now free and fully GPL. The licence layer is gone: no key, no
  activation, no update client of its own — updates come from WordPress like
  any other plugin, and every feature is available to every install.
* Removed every outbound HTTP request. The plugin now talks to nothing but your
  own database.
* Added an "About" screen with the documentation, the live demo, the support
  forum and the author's other plugins, and links to the documentation from the
  Plugins list.
* Hardened the admin request handling: uploaded import files and posted JSON are
  read through explicit unslash-and-validate paths, and every table name reaching
  a query is now formed only from the WordPress table prefix.

= 1.8.1 =
* Fixed: importing an existing custom-fields setup copied no values at all when the field group was placed by page template, by a specific page, on a taxonomy term, on the user form or on an options page. Only the "post type" rule was read, so such a group arrived attached to nothing — it never appeared on an edit screen, and the value importer, which worked from the post types of imported groups, had nothing to walk. Every location rule with an equivalent here is now translated, and the ones without are named in the summary instead of being dropped in silence.
* Fixed: on a multilingual site only one translation of each page was migrated; the others lost their content the moment the old plugin was switched off. Values are now found by asking the meta table which posts hold them, which is independent of the active language and of the group's location.
* Fixed: running the import a second time destroyed everything the first run had imported. Field rows were dropped and re-inserted under fresh ids, and because values are meta keyed by that id, the values were orphaned. Imported fields now carry a stable id derived from their group and field keys, and a re-import updates the rows in place; groups are updated rather than duplicated as "…-2".
* Fixed: values on a post whose type did not match the group's location were skipped. Import considers every imported group and writes only where a value really exists.
* Fixed: a group or field named in Cyrillic ended up with a key like "%d0%b7%d0%b0%d0%b3...", because sanitize_title() percent-encodes what it cannot reduce to ASCII and sanitize_key() drops it entirely. Names are transliterated first, so "Лендинг — главная страница" becomes "lending-glavnaya-stranitsa" and "Заголовок" becomes "zagolovok". Keys already written in a percent-encoded form are renamed in place on the next import, and the values keep working because they are stored against the field's own id. Scripts with no transliteration of their own are still kept as they were.
* Fixed: a post carrying an empty Google Map field could not be saved at all. The map posts its zoom default, so an untouched field looked filled but sanitized to nothing, and validation called that an invalid value.
* Fixed: one invalid field discarded every field on the post, including fields in unrelated groups. Each group is now all-or-nothing on its own; the groups that validate are written.
* Fixed: a required field hidden by conditional logic still blocked the save. Conditions are now evaluated on the server, so a field the editor cannot see is not demanded. Applies to subfields inside repeaters, groups and flexible layouts.
* Fixed: subfield keys written in a non-Latin script were reduced to "_" and then to "__2", "__3". Any script is kept.
* Added: rebuild field groups from an existing custom-fields setup even when the original plugin is gone. Its settings are often deleted with it, leaving values under keys nothing can read; the structure is recovered from the values, types are inferred, and anything that would not survive its guessed type is reported instead of dropped.
* Added: an admin notice when importable custom-fields data is found on the site.

= 1.6.0 =
* Blocks built from a field group, bidirectional relationships, per-group visibility and front-end forms.

= 1.5.0 =
* Field groups can be declared in code or loaded from a JSON directory.
* Field groups reach every post type and taxonomy WordPress exposes.

= 1.4.0 =
* A real settings screen, with diagnostics.
* Shortcodes: the three the documentation had been advertising since 1.0 now
  exist.
* Full Russian and Ukrainian translations.
* Activation is refused on a database too old for the schema.

= 1.3.0 =
* Relationship, user, taxonomy and post-object fields became AJAX search
  pickers instead of rendering hundreds of options into a select.
* The rich-text field is now the real WordPress editor.

= 1.2.0 =
* A familiar, widely-used template API.
* 15 new field types.
* Per-field settings: default value, placeholder, min/max/step and more.

= 1.1.0 =
* Fixed seven paths that silently lost editor data.
* Field lookups cached; a 20-post archive went from 140 queries to 21.
* Repeater rows became queryable through `meta_query`.

= 1.0.0 =
* First release.

== Upgrade Notice ==

= 2.0.1 =
Field settings such as the placeholder no longer come back empty after saving,
and every field can be given a description.

= 2.0.0 =
Ukrainian and Russian ship with the plugin again and follow the admin's
language. Nothing you have configured changes.

= 1.9.9 =
The get_field()/have_rows() compatibility layer is now opt-in. Sites already
using it are left exactly as they were.

= 1.9.8 =
Documentation moved to the plugin's site, and translations move to
translate.wordpress.org. Nothing you have configured changes.

= 1.9.7 =
A field group attached to nothing showed up on every screen, and the rich-text
field's Text tab collapsed to an empty box. Both fixed; stored values are
untouched.

= 1.9.5 =
A field group placed on one page showed up on every screen, and an options page
declared by a theme could not be deleted. Both fixed; stored values are
untouched.


= 1.9.0 =
Now free and GPL, with the licence layer and every outbound request removed.
Existing field groups and values are untouched.
