├── .babelrc.js ├── .browserslistrc ├── .eslintrc.js ├── .gitignore ├── .prettierignore ├── .prettierrc.js ├── LICENSE ├── README.md ├── README.txt ├── assets-repo ├── banner-1544x500.png ├── banner-772x250.png ├── icon-128x128.png ├── icon-256x256.png └── screenshot-1.jpg ├── inc ├── classes │ ├── class-Base.php │ ├── class-GlobalUtils.php │ ├── class-Panel.php │ ├── class-Setting.php │ ├── class-Sidebar.php │ ├── class-Tab.php │ ├── settings │ │ ├── class-Buttons.php │ │ ├── class-Checkbox.php │ │ ├── class-CheckboxMultiple.php │ │ ├── class-Color.php │ │ ├── class-CustomText.php │ │ ├── class-DateRange.php │ │ ├── class-DateSingle.php │ │ ├── class-Image.php │ │ ├── class-ImageMultiple.php │ │ ├── class-Radio.php │ │ ├── class-Range.php │ │ ├── class-RangeFloat.php │ │ ├── class-Select.php │ │ ├── class-Text.php │ │ └── class-Textarea.php │ └── utils │ │ └── utils-methods_call.php ├── core │ ├── core-create_instances.php │ └── core-create_sidebar.php ├── register │ ├── register-create_sidebar.php │ ├── register-enqueue.php │ ├── register-global-utils.php │ └── register-rest.php └── traits │ ├── trait-CastArray.php │ ├── trait-CastSchema.php │ ├── trait-DateLocales.php │ ├── trait-Meta.php │ ├── trait-PrepareOptions.php │ ├── trait-PreparePalette.php │ ├── trait-Sanitize.php │ └── trait-ValidateConditions.php ├── package-lock.json ├── package.json ├── post-meta-controls.php ├── scripts ├── copy-files.js ├── replace-version.js └── webpack_loader-moment.js ├── src ├── classes │ ├── Base.ts │ ├── Panel.ts │ ├── Setting.ts │ ├── Sidebar.tsx │ ├── Tab.ts │ ├── index.ts │ └── settings │ │ ├── Buttons.ts │ │ ├── Checkbox.ts │ │ ├── CheckboxMultiple.ts │ │ ├── Color.ts │ │ ├── CustomText.ts │ │ ├── DateRange.ts │ │ ├── DateSingle.ts │ │ ├── Image.ts │ │ ├── ImageMultiple.ts │ │ ├── Radio.ts │ │ ├── Range.ts │ │ ├── RangeFloat.ts │ │ ├── Select.ts │ │ ├── Text.ts │ │ └── Textarea.ts ├── components │ ├── App │ │ ├── App.styl │ │ ├── App.tsx │ │ ├── _color-fixes.styl │ │ ├── _color-schemes.styl │ │ ├── _logo-fixes.styl │ │ └── index.ts │ ├── Buttons │ │ ├── Buttons.styl │ │ ├── Buttons.tsx │ │ └── index.ts │ ├── Checkbox │ │ ├── Checkbox.tsx │ │ ├── CheckboxMultiple.styl │ │ ├── CheckboxMultiple.tsx │ │ └── index.ts │ ├── Color │ │ ├── Color.styl │ │ ├── Color.tsx │ │ └── index.ts │ ├── CustomText │ │ ├── CustomText.styl │ │ ├── CustomText.tsx │ │ └── index.ts │ ├── Date │ │ ├── Date.styl │ │ ├── DateRange.tsx │ │ ├── DateSingle.tsx │ │ └── index.ts │ ├── Image │ │ ├── Image.styl │ │ ├── Image.tsx │ │ ├── ImageMultiple.tsx │ │ └── index.ts │ ├── Panel │ │ ├── PanelCollapsible.tsx │ │ ├── PanelNotCollapsible.tsx │ │ ├── Panels.styl │ │ ├── Panels.tsx │ │ └── index.ts │ ├── Radio │ │ ├── Radio.tsx │ │ └── index.ts │ ├── Range │ │ ├── Range.styl │ │ ├── Range.tsx │ │ ├── RangeFloat.tsx │ │ └── index.ts │ ├── Select │ │ ├── Select.styl │ │ ├── Select.tsx │ │ └── index.ts │ ├── Setting │ │ ├── Setting.tsx │ │ ├── Settings.styl │ │ ├── Settings.tsx │ │ ├── index.ts │ │ ├── withLocalData.tsx │ │ ├── withMetaData.tsx │ │ └── withNoneData.tsx │ ├── Sidebar │ │ ├── Sidebar.tsx │ │ └── index.ts │ ├── Tab │ │ ├── Tab.tsx │ │ ├── Tabs.styl │ │ ├── Tabs.tsx │ │ └── index.ts │ ├── Text │ │ ├── Text.tsx │ │ └── index.ts │ ├── Textarea │ │ ├── Textarea.tsx │ │ └── index.ts │ └── Warnings │ │ ├── Warning.tsx │ │ ├── Warnings.styl │ │ ├── Warnings.tsx │ │ └── index.ts ├── entry.ts ├── init │ ├── register-items.ts │ └── register-store.ts ├── store │ ├── actions.ts │ ├── reducer.ts │ └── selectors.ts └── utils │ ├── components │ ├── A.tsx │ ├── Button.tsx │ ├── Div.tsx │ ├── H3.tsx │ ├── Icon.tsx │ ├── Img.tsx │ ├── Li.tsx │ ├── Ol.tsx │ ├── P.tsx │ ├── Span.tsx │ ├── Ul.tsx │ └── index.ts │ ├── data │ ├── icons.tsx │ ├── index.ts │ ├── plugin.ts │ └── stylus_variables.styl │ └── tools │ ├── addPrefix.ts │ ├── castSchema.js │ ├── getColorScheme.ts │ ├── index.ts │ ├── prepareIcon.tsx │ ├── prepareImageData.ts │ ├── prepareOptions.ts │ ├── prepareProps.ts │ └── sanitize.ts ├── tsconfig.json ├── types ├── Base.d.ts ├── Panel.d.ts ├── Setting-Buttons.d.ts ├── Setting-Checkbox.d.ts ├── Setting-CheckboxMultiple.d.ts ├── Setting-Color.d.ts ├── Setting-CustomText.d.ts ├── Setting-DateRange.d.ts ├── Setting-DateSingle.d.ts ├── Setting-Image.d.ts ├── Setting-ImageMultiple.d.ts ├── Setting-Radio.d.ts ├── Setting-Range.d.ts ├── Setting-RangeFloat.d.ts ├── Setting-Select.d.ts ├── Setting-Text.d.ts ├── Setting-Textarea.d.ts ├── Setting.d.ts ├── Sidebar.d.ts ├── Tab.d.ts ├── others.d.ts ├── store-actions.d.ts ├── store-selectors.d.ts ├── store-state.d.ts └── utils.d.ts └── webpack.config.js /.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ["@babel/plugin-proposal-class-properties"], 3 | 4 | presets: [ 5 | // Uses .browserslistrc info 6 | "@babel/preset-env", 7 | "@babel/preset-react", 8 | "@babel/preset-typescript", 9 | ], 10 | }; 11 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | extends @wordpress/browserslist-config 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: "@typescript-eslint/parser", 3 | 4 | ignorePatterns: ["_extras", "_release", "dist", "node_modules", "pro"], 5 | 6 | extends: [ 7 | "eslint:recommended", 8 | "plugin:react/recommended", 9 | "plugin:@typescript-eslint/recommended", 10 | "plugin:prettier/recommended", 11 | "prettier/@typescript-eslint", 12 | "prettier/react", 13 | ], 14 | 15 | env: { 16 | es2020: true, 17 | browser: true, 18 | node: true, 19 | }, 20 | 21 | rules: { 22 | "@typescript-eslint/explicit-function-return-type": "off", 23 | "@typescript-eslint/camelcase": "off", 24 | "@typescript-eslint/no-var-requires": "off", 25 | "@typescript-eslint/ban-ts-ignore": "off", 26 | "react/react-in-jsx-scope": "off", 27 | "react/prop-types": "off", 28 | "react/display-name": "off", 29 | }, 30 | 31 | settings: { 32 | react: { 33 | version: "16.13.1", // Version used in WP 5.6 34 | }, 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .todo 3 | /_extras 4 | /_release 5 | /dist 6 | /node_modules 7 | /pro 8 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /_extras 2 | /_release 3 | /dist 4 | /node_modules 5 | /pro 6 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | useTabs: true, 3 | tabWidth: 4, 4 | arrowParens: "avoid", 5 | }; 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Banner Image](assets-repo/banner-1544x500.png) 2 | 3 | # Post Meta Controls 4 | 5 | [Demo](https://gutenberg-showcase.melonpan.io/post-meta-controls) - [Documentation](https://melonpan.io/wordpress-plugins/post-meta-controls) - [WordPress](https://wordpress.org/plugins/post-meta-controls) 6 | 7 |
8 | 9 | WordPress plugin that provides utilities to register, save and modify post meta data in the Gutenberg editor. 10 | 11 | Register, Save, Modify and Get meta data in the Gutenberg editor. 12 | Use this plugin to add meta data controls inside a sidebar in the editor of posts, pages or custom post types. 13 | 14 | The plugin comes with different options to customize the Sidebars, Tabs, Panels and Setting controls. 15 | 16 |
17 | 18 | ## Features 19 | 20 | This is the list of controls available: 21 | 22 | - Buttons 23 | - Checkbox, Checkbox Multiple 24 | - Color, Color with Alpha 25 | - Custom text 26 | - Date Range, Date Single 27 | - Image, Image Multiple 28 | - Radio 29 | - Range, Range with Float number 30 | - Select 31 | - Text, Textarea 32 | 33 |
34 | 35 | ## Screenshots 36 | 37 | Sidebar with different tabs, panels and controls 38 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | === Post Meta Controls === 2 | Contributors: melonpan 3 | Tags: gutenberg, meta, post-meta, settings, controls 4 | Requires at least: 5.2 5 | Tested up to: 5.3 6 | Stable tag: 1.4.1 7 | Requires PHP: 7.1 8 | License: GPLv3 9 | License URI: https://www.gnu.org/licenses/gpl-3.0.html 10 | 11 | Utilities to register, save and modify post meta data in the Gutenberg editor. 12 | 13 | 14 | == Description == 15 | 16 | [Demo](https://gutenberg-showcase.melonpan.io/post-meta-controls) - [Documentation](https://melonpan.io/wordpress-plugins/post-meta-controls) - [GitHub](https://github.com/garciaalvaro/post-meta-controls) 17 | 18 | Register, Save, Modify and Get meta data in the Gutenberg editor. 19 | Use this plugin to add meta data controls inside a sidebar in the editor of posts, pages or custom post types. 20 | This is the list of controls available: 21 | 22 | * Buttons 23 | * Checkbox, Checkbox Multiple 24 | * Color, Color with Alpha 25 | * Custom text 26 | * Date Range, Date Single 27 | * Image, Image Multiple 28 | * Radio 29 | * Range, Range with Float number 30 | * Select 31 | * Text, Textarea 32 | 33 | The plugin comes with different options to customize the Sidebars, Tabs, Panels and Setting controls. 34 | 35 | 36 | == Usage == 37 | 38 | Once the plugin is installed, you will need to include the plugin filter inside your plugin or theme to create a sidebar with it's settings. 39 | The new sidebar/s can be accessed in any post type where it was registered. 40 | Modify the setting values with the controls inside the sidebar. 41 | Use the plugin helpers (see *Helpers to get the meta values* section) to get the meta data in the front end. 42 | 43 | 44 | == Installation == 45 | 46 | Installation from the WordPress admin. 47 | 48 | 1. Log in to the WordPress admin and navigate to *Plugins > Add New*. 49 | 2. Type *Post Meta Controls* in the Search field. 50 | 3. In the results list *Post Meta Controls* plugin should appear, click **Install Now** button. 51 | 4. Once it finished installing, click the *Activate* button. 52 | 5. Now you can register your sidebar and settings using the filter in your plugin or theme. 53 | 6. To view your sidebar go to any post where Gutenberg is enabled and the sidebar was registered to. 54 | 55 | 56 | == Screenshots == 57 | 58 | 1. Sidebar with different tabs, panels and controls 59 | 60 | 61 | == Changelog == 62 | 63 | = 1.4.1 = 64 | * Updated dependencies 65 | 66 | = 1.4.0 = 67 | * Updated dependencies 68 | * Minor bug fixes 69 | 70 | = 1.3.4 = 71 | * Fixed bug with color default value not saving 72 | * Fixed initial data fetch 73 | * Minor style fixes for WP 5.6 74 | 75 | = 1.3.3 = 76 | * Fixed bug with case sensitive file name mismatch 77 | 78 | = 1.3.2 = 79 | * Fixed incompatibilities with WordPress 5.5 80 | * Updated packages 81 | 82 | = 1.3.1 = 83 | * Fixed bug that didn't load correctly small images in the editor. 84 | * Minor bug fixes. 85 | 86 | = 1.3.0 = 87 | * Added minimum_days option in date_range. 88 | * Added maximum_days option in date_range. 89 | 90 | = 1.2.0 = 91 | * Added unavailable_dates option in date_single and date_range. 92 | * Use a rest route to get the sidebars data instead of printing the data inline. 93 | * Fixed WP 5.3 meta key from saving an empty value if the key doesnt exist. 94 | * Fixed momentjs locales file not loading correctly. 95 | * Fixed date_range defaults not showing. 96 | * Fixed bug when saving empty value in image and image_multiple. 97 | * Code refactor. Migrated JavaScript to TypeScript. 98 | 99 | = 1.1.0 = 100 | * Simplified some of the core functions. 101 | * Styling fixes. 102 | * Minor bug fixes. 103 | * Updated dependencies. 104 | 105 | = 1.0.1 = 106 | * Checkbox Multiple fix: If there were old values saved that no longer belong to the options, we display them as selected. If they are deselected we remove them from the options. 107 | * Updated dependencies. 108 | 109 | = 1.0.0 = 110 | * Initial release. 111 | -------------------------------------------------------------------------------- /assets-repo/banner-1544x500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/garciaalvaro/post-meta-controls/c33ffd5e56e465e1162de9fbe32773545c71ad8a/assets-repo/banner-1544x500.png -------------------------------------------------------------------------------- /assets-repo/banner-772x250.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/garciaalvaro/post-meta-controls/c33ffd5e56e465e1162de9fbe32773545c71ad8a/assets-repo/banner-772x250.png -------------------------------------------------------------------------------- /assets-repo/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/garciaalvaro/post-meta-controls/c33ffd5e56e465e1162de9fbe32773545c71ad8a/assets-repo/icon-128x128.png -------------------------------------------------------------------------------- /assets-repo/icon-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/garciaalvaro/post-meta-controls/c33ffd5e56e465e1162de9fbe32773545c71ad8a/assets-repo/icon-256x256.png -------------------------------------------------------------------------------- /assets-repo/screenshot-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/garciaalvaro/post-meta-controls/c33ffd5e56e465e1162de9fbe32773545c71ad8a/assets-repo/screenshot-1.jpg -------------------------------------------------------------------------------- /inc/classes/class-Base.php: -------------------------------------------------------------------------------- 1 | props = $props; 25 | 26 | // Remove keys that are meant to be assigned by the class. 27 | $this->set_privates(); 28 | $this->unset_private_keys(); 29 | 30 | // Run functions before set defaults. 31 | if (method_exists($this, "before_set_defaults")) { 32 | $this->before_set_defaults(); 33 | } 34 | 35 | // Defaults. 36 | $this->set_defaults(); 37 | $this->merge_defaults(); 38 | 39 | // Run functions before set schema. 40 | if (method_exists($this, "before_set_schema")) { 41 | $this->before_set_schema(); 42 | } 43 | 44 | // Schema. 45 | $this->set_schema(); 46 | $this->cast_props(); 47 | 48 | // Run functions before cast props. 49 | if (method_exists($this, "after_cast_props")) { 50 | $this->after_cast_props(); 51 | } 52 | 53 | $this->validate_props(); 54 | 55 | // Run functions after validate props. 56 | if (method_exists($this, "after_validate_props")) { 57 | $this->after_validate_props(); 58 | } 59 | } 60 | 61 | abstract protected function set_defaults(); 62 | abstract protected function set_schema(); 63 | 64 | protected function set_privates() 65 | { 66 | } 67 | 68 | /** 69 | * Unset prop keys which are meant to be private. 70 | */ 71 | private function unset_private_keys() 72 | { 73 | if (is_null($this->props_privates)) { 74 | return; 75 | } 76 | 77 | foreach ($this->props_privates as $private) { 78 | if (isset($this->props[$private])) { 79 | unset($this->props[$private]); 80 | } 81 | } 82 | } 83 | 84 | /** 85 | * Assign default properties if not present in the given array and 86 | * remove keys which are not present in $props_defaults. 87 | */ 88 | private function merge_defaults() 89 | { 90 | $this->props = shortcode_atts($this->props_defaults, $this->props); 91 | } 92 | 93 | /** 94 | * Cast the given properties to the required schema. 95 | */ 96 | private function cast_props() 97 | { 98 | $this->props = $this->cast_schema($this->props, $this->props_schema); 99 | } 100 | 101 | /** 102 | * Set the validity of the class checking if each prop fits the given conditions. 103 | */ 104 | private function validate_props() 105 | { 106 | $is_valid = $this->validate_conditions( 107 | $this->props, 108 | $this->props_schema 109 | ); 110 | 111 | $this->props["valid"] = $is_valid; 112 | } 113 | 114 | /** 115 | * Set the prefix in the id prop. 116 | */ 117 | protected function set_id_with_prefix() 118 | { 119 | if (empty($this->props["id"]) || empty($this->props["id_prefix"])) { 120 | return; 121 | } 122 | 123 | $this->props["id"] = $this->props["id_prefix"] . $this->props["id"]; 124 | } 125 | 126 | public function get_id() 127 | { 128 | return $this->props["id"]; 129 | } 130 | 131 | public function get_post_type() 132 | { 133 | return $this->props["post_type"]; 134 | } 135 | 136 | /** 137 | * Get props which are meant to be sent to the editor. 138 | */ 139 | public function get_props_for_js() 140 | { 141 | $props_for_js = []; 142 | 143 | foreach ($this->props as $key => $value) { 144 | if ( 145 | isset($this->props_schema[$key]["for_js"]) && 146 | true === $this->props_schema[$key]["for_js"] 147 | ) { 148 | $props_for_js[$key] = $value; 149 | } 150 | } 151 | 152 | return $props_for_js; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /inc/classes/class-Panel.php: -------------------------------------------------------------------------------- 1 | set_id_with_prefix(); 18 | } 19 | 20 | protected function set_defaults() 21 | { 22 | $this->props_defaults = [ 23 | "id" => wp_generate_uuid4(), 24 | "id_prefix" => "", 25 | "path" => [], 26 | "label" => "", 27 | "post_type" => "post", // It will be passed through cast_array(). 28 | "initial_open" => true, 29 | "collapsible" => true, 30 | "icon_dashicon" => "", 31 | "icon_svg" => "", 32 | ]; 33 | } 34 | 35 | protected function set_schema() 36 | { 37 | $this->props_schema = [ 38 | "id" => [ 39 | "type" => "id", 40 | "for_js" => true, 41 | "conditions" => "not_empty", 42 | ], 43 | "id_prefix" => [ 44 | "type" => "id", 45 | "for_js" => true, 46 | ], 47 | "path" => [ 48 | "type" => ["_all" => "id"], 49 | "for_js" => true, 50 | "conditions" => "not_empty", 51 | ], 52 | "label" => [ 53 | "type" => "text", 54 | "for_js" => true, 55 | "conditions" => 56 | true === $this->props["collapsible"] ? "not_empty" : false, 57 | ], 58 | "post_type" => [ 59 | "type" => ["_all" => "id"], 60 | "for_js" => false, 61 | ], 62 | "initial_open" => [ 63 | "type" => "boolean", 64 | "for_js" => true, 65 | ], 66 | "collapsible" => [ 67 | "type" => "boolean", 68 | "for_js" => true, 69 | ], 70 | "icon_dashicon" => [ 71 | "type" => "id", 72 | "for_js" => true, 73 | ], 74 | "icon_svg" => [ 75 | "type" => "html_svg", 76 | "for_js" => true, 77 | ], 78 | ]; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /inc/classes/class-Sidebar.php: -------------------------------------------------------------------------------- 1 | set_id_with_prefix(); 18 | } 19 | 20 | public function get_data_key_prefix() 21 | { 22 | return $this->props["data_key_prefix"]; 23 | } 24 | 25 | public function get_id_prefix() 26 | { 27 | return $this->props["id_prefix"]; 28 | } 29 | 30 | protected function set_defaults() 31 | { 32 | $this->props_defaults = [ 33 | "id" => wp_generate_uuid4(), 34 | "id_prefix" => "", 35 | "label" => "", 36 | "post_type" => "post", // It will be passed through cast_array(). 37 | "data_key_prefix" => "pmc_", 38 | "icon_dashicon" => "carrot", 39 | "icon_svg" => "", 40 | "ui_color_scheme" => "light", 41 | ]; 42 | } 43 | 44 | protected function set_schema() 45 | { 46 | $this->props_schema = [ 47 | "id" => [ 48 | "type" => "id", 49 | "for_js" => true, 50 | "conditions" => "not_empty", 51 | ], 52 | "id_prefix" => [ 53 | "type" => "id", 54 | "for_js" => true, 55 | ], 56 | "label" => [ 57 | "type" => "text", 58 | "for_js" => true, 59 | "conditions" => "not_empty", 60 | ], 61 | "post_type" => [ 62 | "type" => ["_all" => "id"], 63 | "for_js" => false, 64 | ], 65 | "data_key_prefix" => [ 66 | "type" => "id", 67 | "for_js" => false, 68 | ], 69 | "icon_dashicon" => [ 70 | "type" => "id", 71 | "for_js" => true, 72 | ], 73 | "icon_svg" => [ 74 | "type" => "html_svg", 75 | "for_js" => true, 76 | ], 77 | "ui_color_scheme" => [ 78 | "type" => "id", 79 | "for_js" => true, 80 | ], 81 | ]; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /inc/classes/class-Tab.php: -------------------------------------------------------------------------------- 1 | set_id_with_prefix(); 15 | } 16 | 17 | protected function set_defaults() 18 | { 19 | $this->props_defaults = [ 20 | "id" => wp_generate_uuid4(), 21 | "id_prefix" => "", 22 | "path" => [], 23 | "label" => "", 24 | "post_type" => "post", // It will be passed through cast_array(). 25 | "icon_dashicon" => "", 26 | "icon_svg" => "", 27 | ]; 28 | } 29 | 30 | protected function set_schema() 31 | { 32 | $this->props_schema = [ 33 | "id" => [ 34 | "type" => "id", 35 | "for_js" => true, 36 | "conditions" => "not_empty", 37 | ], 38 | "id_prefix" => [ 39 | "type" => "id", 40 | "for_js" => true, 41 | ], 42 | "path" => [ 43 | "type" => ["_all" => "id"], 44 | "for_js" => true, 45 | "conditions" => "not_empty", 46 | ], 47 | "label" => [ 48 | "type" => "text", 49 | "for_js" => true, 50 | "conditions" => "not_empty", 51 | ], 52 | "post_type" => [ 53 | "type" => ["_all" => "id"], 54 | "for_js" => false, 55 | ], 56 | "icon_dashicon" => [ 57 | "type" => "id", 58 | "for_js" => true, 59 | ], 60 | "icon_svg" => [ 61 | "type" => "html_svg", 62 | "for_js" => true, 63 | ], 64 | ]; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /inc/classes/settings/class-Buttons.php: -------------------------------------------------------------------------------- 1 | "buttons", 19 | "default_value" => "", 20 | "allow_empty" => false, 21 | "options" => [], 22 | ]; 23 | 24 | $parent_defaults = Setting::get_defaults(); 25 | 26 | $this->props_defaults = wp_parse_args($this_defaults, $parent_defaults); 27 | } 28 | 29 | protected function set_schema() 30 | { 31 | $this_schema = [ 32 | "default_value" => [ 33 | "type" => "id", 34 | "for_js" => true, 35 | ], 36 | "allow_empty" => [ 37 | "type" => "boolean", 38 | "for_js" => true, 39 | ], 40 | "options" => [ 41 | "type" => [ 42 | "_all" => [ 43 | "value" => "id", 44 | "title" => "text", 45 | "icon_dashicon" => "id", 46 | "icon_svg" => "html_svg", 47 | ], 48 | ], 49 | "for_js" => true, 50 | "conditions" => "not_empty", 51 | ], 52 | ]; 53 | 54 | $parent_schema = Setting::get_schema(); 55 | 56 | $this->props_schema = wp_parse_args($this_schema, $parent_schema); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /inc/classes/settings/class-Checkbox.php: -------------------------------------------------------------------------------- 1 | "checkbox", 19 | "default_value" => false, 20 | "input_label" => "", 21 | "use_toggle" => false, 22 | ]; 23 | 24 | $parent_defaults = Setting::get_defaults(); 25 | 26 | $this->props_defaults = wp_parse_args($this_defaults, $parent_defaults); 27 | } 28 | 29 | protected function set_schema() 30 | { 31 | $this_schema = [ 32 | "default_value" => [ 33 | "type" => "boolean", 34 | "for_js" => true, 35 | ], 36 | "input_label" => [ 37 | "type" => "text", 38 | "for_js" => true, 39 | "conditions" => "not_empty", 40 | ], 41 | "use_toggle" => [ 42 | "type" => "boolean", 43 | "for_js" => true, 44 | ], 45 | ]; 46 | 47 | $parent_schema = Setting::get_schema(); 48 | 49 | $this->props_schema = wp_parse_args($this_schema, $parent_schema); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /inc/classes/settings/class-CheckboxMultiple.php: -------------------------------------------------------------------------------- 1 | props["options"] = $this->prepare_options( 21 | $this->props["options"] 22 | ); 23 | } 24 | 25 | protected function set_defaults() 26 | { 27 | $this_defaults = [ 28 | "type" => "checkbox_multiple", 29 | "default_value" => "", // It will be passed through cast_array(). 30 | "options" => [], 31 | "use_toggle" => false, 32 | ]; 33 | 34 | $parent_defaults = Setting::get_defaults(); 35 | 36 | $this->props_defaults = wp_parse_args($this_defaults, $parent_defaults); 37 | } 38 | 39 | protected function set_schema() 40 | { 41 | $this_schema = [ 42 | "default_value" => [ 43 | "type" => ["_all" => "id"], 44 | "for_js" => true, 45 | ], 46 | "options" => [ 47 | "type" => [ 48 | "_all" => [ 49 | "value" => "id", 50 | "label" => "text", 51 | ], 52 | ], 53 | "for_js" => true, 54 | "conditions" => "not_empty", 55 | ], 56 | "use_toggle" => [ 57 | "type" => "boolean", 58 | "for_js" => true, 59 | ], 60 | ]; 61 | 62 | $parent_schema = Setting::get_schema(); 63 | 64 | $this->props_schema = wp_parse_args($this_schema, $parent_schema); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /inc/classes/settings/class-Color.php: -------------------------------------------------------------------------------- 1 | props["palette"] = $this->prepare_palette( 21 | $this->props["palette"] 22 | ); 23 | } 24 | 25 | protected function set_defaults() 26 | { 27 | $this_defaults = [ 28 | "type" => "color", 29 | "default_value" => "", 30 | "alpha_control" => false, 31 | "palette" => [], 32 | ]; 33 | 34 | $parent_defaults = Setting::get_defaults(); 35 | 36 | $this->props_defaults = wp_parse_args($this_defaults, $parent_defaults); 37 | } 38 | 39 | protected function set_schema() 40 | { 41 | $this_schema = [ 42 | "default_value" => [ 43 | "type" => "text", 44 | "for_js" => true, 45 | ], 46 | "alpha_control" => [ 47 | "type" => "boolean", 48 | "for_js" => true, 49 | ], 50 | "palette" => [ 51 | "type" => [ 52 | "_all" => [ 53 | "name" => "id", 54 | "color" => "text", 55 | ], 56 | ], 57 | "for_js" => true, 58 | ], 59 | ]; 60 | 61 | $parent_schema = Setting::get_schema(); 62 | 63 | $this->props_schema = wp_parse_args($this_schema, $parent_schema); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /inc/classes/settings/class-CustomText.php: -------------------------------------------------------------------------------- 1 | "custom_text", 19 | "content" => "", 20 | ]; 21 | 22 | $parent_defaults = Setting::get_defaults(); 23 | 24 | $this->props_defaults = wp_parse_args($this_defaults, $parent_defaults); 25 | } 26 | 27 | protected function set_schema() 28 | { 29 | $this_schema = [ 30 | "content" => [ 31 | "type" => [ 32 | "_all" => [ 33 | "type" => "id", 34 | "content" => ["_all" => "text"], 35 | "href" => "text", 36 | ], 37 | ], 38 | "for_js" => true, 39 | "conditions" => "not_empty", 40 | ], 41 | ]; 42 | 43 | $parent_schema = Setting::get_schema(); 44 | 45 | $this->props_schema = wp_parse_args($this_schema, $parent_schema); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /inc/classes/settings/class-DateRange.php: -------------------------------------------------------------------------------- 1 | "date_range", 21 | "default_value" => [], 22 | "format" => "DD/MM/YYYY", 23 | "locale" => "en", 24 | "unavailable_dates" => [["before", "today"]], 25 | "minimum_days" => 1, 26 | "maximum_days" => 0, 27 | ]; 28 | 29 | $parent_defaults = Setting::get_defaults(); 30 | 31 | $this->props_defaults = wp_parse_args($this_defaults, $parent_defaults); 32 | } 33 | 34 | protected function set_schema() 35 | { 36 | $this_schema = [ 37 | "default_value" => [ 38 | "type" => ["_all" => "text"], 39 | "for_js" => true, 40 | ], 41 | "format" => [ 42 | "type" => "text", 43 | "for_js" => true, 44 | ], 45 | "locale" => [ 46 | "type" => "id", 47 | "for_js" => true, 48 | ], 49 | "unavailable_dates" => [ 50 | "type" => ["_all" => ["_all" => "text"]], 51 | "for_js" => true, 52 | ], 53 | "minimum_days" => [ 54 | "type" => "integer", 55 | "for_js" => true, 56 | ], 57 | "maximum_days" => [ 58 | "type" => "integer", 59 | "for_js" => true, 60 | ], 61 | ]; 62 | 63 | $parent_schema = Setting::get_schema(); 64 | 65 | $this->props_schema = wp_parse_args($this_schema, $parent_schema); 66 | } 67 | 68 | public function enqueue_locale() 69 | { 70 | $locale = $this->props["locale"]; 71 | $locales = $this->get_date_locales(); 72 | 73 | if (false === in_array($locale, $locales)) { 74 | return; 75 | } 76 | 77 | // Add the action to enqueue the locale script. 78 | add_action("pmc_before_enqueue", function () { 79 | // Enqueue the selected locale script. 80 | wp_enqueue_script( 81 | PLUGIN_NAME . "-moment-locales", 82 | DIST_DIR . PLUGIN_NAME . "-moment-locales.js", 83 | [], 84 | PLUGIN_VERSION, 85 | true // Enqueue in the footer. 86 | ); 87 | }); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /inc/classes/settings/class-DateSingle.php: -------------------------------------------------------------------------------- 1 | "date_single", 21 | "default_value" => "", 22 | "format" => "DD/MM/YYYY", 23 | "locale" => "en", 24 | "unavailable_dates" => [["before", "today"]], 25 | ]; 26 | 27 | $parent_defaults = Setting::get_defaults(); 28 | 29 | $this->props_defaults = wp_parse_args($this_defaults, $parent_defaults); 30 | } 31 | 32 | protected function set_schema() 33 | { 34 | $this_schema = [ 35 | "default_value" => [ 36 | "type" => "text", 37 | "for_js" => true, 38 | ], 39 | "format" => [ 40 | "type" => "text", 41 | "for_js" => true, 42 | ], 43 | "locale" => [ 44 | "type" => "id", 45 | "for_js" => true, 46 | ], 47 | "unavailable_dates" => [ 48 | "type" => ["_all" => ["_all" => "text"]], 49 | "for_js" => true, 50 | ], 51 | ]; 52 | 53 | $parent_schema = Setting::get_schema(); 54 | 55 | $this->props_schema = wp_parse_args($this_schema, $parent_schema); 56 | } 57 | 58 | public function enqueue_locale() 59 | { 60 | $locale = $this->props["locale"]; 61 | $locales = $this->get_date_locales(); 62 | 63 | if (false === in_array($locale, $locales)) { 64 | return; 65 | } 66 | 67 | // Add the action to enqueue the locale script. 68 | add_action("pmc_before_enqueue", function () { 69 | // Enqueue the selected locale script. 70 | wp_enqueue_script( 71 | PLUGIN_NAME . "-moment-locales", 72 | DIST_DIR . PLUGIN_NAME . "-moment-locales.js", 73 | [], 74 | PLUGIN_VERSION, 75 | true // Enqueue in the footer. 76 | ); 77 | }); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /inc/classes/settings/class-Image.php: -------------------------------------------------------------------------------- 1 | "image", 19 | "default_value" => 0, 20 | ]; 21 | 22 | $parent_defaults = Setting::get_defaults(); 23 | 24 | $this->props_defaults = wp_parse_args($this_defaults, $parent_defaults); 25 | } 26 | 27 | protected function set_schema() 28 | { 29 | $this_schema = [ 30 | "default_value" => [ 31 | "type" => "integer", 32 | "for_js" => true, 33 | ], 34 | ]; 35 | 36 | $parent_schema = Setting::get_schema(); 37 | 38 | $this->props_schema = wp_parse_args($this_schema, $parent_schema); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /inc/classes/settings/class-ImageMultiple.php: -------------------------------------------------------------------------------- 1 | "image_multiple", 19 | "default_value" => [], 20 | ]; 21 | 22 | $parent_defaults = Setting::get_defaults(); 23 | 24 | $this->props_defaults = wp_parse_args($this_defaults, $parent_defaults); 25 | } 26 | 27 | protected function set_schema() 28 | { 29 | $this_schema = [ 30 | "default_value" => [ 31 | "type" => ["_all" => "integer"], 32 | "for_js" => true, 33 | ], 34 | ]; 35 | 36 | $parent_schema = Setting::get_schema(); 37 | 38 | $this->props_schema = wp_parse_args($this_schema, $parent_schema); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /inc/classes/settings/class-Radio.php: -------------------------------------------------------------------------------- 1 | props["options"] = $this->prepare_options( 21 | $this->props["options"] 22 | ); 23 | } 24 | 25 | protected function set_defaults() 26 | { 27 | $this_defaults = [ 28 | "type" => "radio", 29 | "default_value" => "", 30 | "options" => [], 31 | ]; 32 | 33 | $parent_defaults = Setting::get_defaults(); 34 | 35 | $this->props_defaults = wp_parse_args($this_defaults, $parent_defaults); 36 | } 37 | 38 | protected function set_schema() 39 | { 40 | $this_schema = [ 41 | "default_value" => [ 42 | "type" => "id", 43 | "for_js" => true, 44 | ], 45 | "options" => [ 46 | "type" => [ 47 | "_all" => [ 48 | "value" => "id", 49 | "label" => "text", 50 | ], 51 | ], 52 | "for_js" => true, 53 | "conditions" => "not_empty", 54 | ], 55 | ]; 56 | 57 | $parent_schema = Setting::get_schema(); 58 | 59 | $this->props_schema = wp_parse_args($this_schema, $parent_schema); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /inc/classes/settings/class-Range.php: -------------------------------------------------------------------------------- 1 | "range", 19 | "default_value" => 50, 20 | "step" => 1, 21 | "min" => 0, 22 | "max" => 100, 23 | ]; 24 | 25 | $parent_defaults = Setting::get_defaults(); 26 | 27 | $this->props_defaults = wp_parse_args($this_defaults, $parent_defaults); 28 | } 29 | 30 | protected function set_schema() 31 | { 32 | $this_schema = [ 33 | "default_value" => [ 34 | "type" => "integer", 35 | "for_js" => true, 36 | ], 37 | "step" => [ 38 | "type" => "integer", 39 | "for_js" => true, 40 | "conditions" => [ 41 | $this->props["step"] > 0, 42 | $this->props["max"] - $this->props["min"] > 43 | $this->props["step"], 44 | ], 45 | ], 46 | "min" => [ 47 | "type" => "integer", 48 | "for_js" => true, 49 | ], 50 | "max" => [ 51 | "type" => "integer", 52 | "for_js" => true, 53 | "conditions" => $this->props["max"] > $this->props["min"], 54 | ], 55 | ]; 56 | 57 | $parent_schema = Setting::get_schema(); 58 | 59 | $this->props_schema = wp_parse_args($this_schema, $parent_schema); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /inc/classes/settings/class-RangeFloat.php: -------------------------------------------------------------------------------- 1 | "range_float", 19 | "default_value" => 50, 20 | "step" => 1, 21 | "min" => 0, 22 | "max" => 100, 23 | ]; 24 | 25 | $parent_defaults = Setting::get_defaults(); 26 | 27 | $this->props_defaults = wp_parse_args($this_defaults, $parent_defaults); 28 | } 29 | 30 | protected function set_schema() 31 | { 32 | $this_schema = [ 33 | "default_value" => [ 34 | "type" => "float", 35 | "for_js" => true, 36 | ], 37 | "step" => [ 38 | "type" => "float", 39 | "for_js" => true, 40 | "conditions" => [ 41 | $this->props["step"] > 0, 42 | $this->props["max"] - $this->props["min"] > 43 | $this->props["step"], 44 | ], 45 | ], 46 | "min" => [ 47 | "type" => "float", 48 | "for_js" => true, 49 | ], 50 | "max" => [ 51 | "type" => "float", 52 | "for_js" => true, 53 | "conditions" => $this->props["max"] > $this->props["min"], 54 | ], 55 | ]; 56 | 57 | $parent_schema = Setting::get_schema(); 58 | 59 | $this->props_schema = wp_parse_args($this_schema, $parent_schema); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /inc/classes/settings/class-Select.php: -------------------------------------------------------------------------------- 1 | props["options"] = $this->prepare_options( 21 | $this->props["options"] 22 | ); 23 | } 24 | 25 | protected function set_defaults() 26 | { 27 | $this_defaults = [ 28 | "type" => "select", 29 | "default_value" => "", 30 | "options" => [], 31 | ]; 32 | 33 | $parent_defaults = Setting::get_defaults(); 34 | 35 | $this->props_defaults = wp_parse_args($this_defaults, $parent_defaults); 36 | } 37 | 38 | protected function set_schema() 39 | { 40 | $this_schema = [ 41 | "default_value" => [ 42 | "type" => "id", 43 | "for_js" => true, 44 | ], 45 | "options" => [ 46 | "type" => [ 47 | "_all" => [ 48 | "value" => "id", 49 | "label" => "text", 50 | ], 51 | ], 52 | "for_js" => true, 53 | "conditions" => "not_empty", 54 | ], 55 | ]; 56 | 57 | $parent_schema = Setting::get_schema(); 58 | 59 | $this->props_schema = wp_parse_args($this_schema, $parent_schema); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /inc/classes/settings/class-Text.php: -------------------------------------------------------------------------------- 1 | "text", 19 | "default_value" => "", 20 | "placeholder" => "", 21 | ]; 22 | 23 | $parent_defaults = Setting::get_defaults(); 24 | 25 | $this->props_defaults = wp_parse_args($this_defaults, $parent_defaults); 26 | } 27 | 28 | protected function set_schema() 29 | { 30 | $this_schema = [ 31 | "default_value" => [ 32 | "type" => "text", 33 | "for_js" => true, 34 | ], 35 | "placeholder" => [ 36 | "type" => "text", 37 | "for_js" => true, 38 | ], 39 | ]; 40 | 41 | $parent_schema = Setting::get_schema(); 42 | 43 | $this->props_schema = wp_parse_args($this_schema, $parent_schema); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /inc/classes/settings/class-Textarea.php: -------------------------------------------------------------------------------- 1 | "textarea", 19 | "default_value" => "", 20 | "placeholder" => "", 21 | ]; 22 | 23 | $parent_defaults = Setting::get_defaults(); 24 | 25 | $this->props_defaults = wp_parse_args($this_defaults, $parent_defaults); 26 | } 27 | 28 | protected function set_schema() 29 | { 30 | $this_schema = [ 31 | "default_value" => [ 32 | "type" => "textarea", 33 | "for_js" => true, 34 | ], 35 | "placeholder" => [ 36 | "type" => "textarea", 37 | "for_js" => true, 38 | ], 39 | ]; 40 | 41 | $parent_schema = Setting::get_schema(); 42 | 43 | $this->props_schema = wp_parse_args($this_schema, $parent_schema); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /inc/classes/utils/utils-methods_call.php: -------------------------------------------------------------------------------- 1 | get_data_key_with_prefix(); 21 | 22 | // We only register the first setting with a certain meta_key, 23 | // although we let it be modified with other setting controls inside js. 24 | if (!in_array($data_key, $data_key_array)) { 25 | $setting_instance->register_meta(); 26 | 27 | $data_key_array[] = $data_key; 28 | } 29 | } 30 | } 31 | 32 | /** 33 | * Trigger the enqueue_locale method and enqueue date scripts. 34 | * 35 | * @since 1.0.0 36 | */ 37 | function enqueue_locale($setting_instances = []) 38 | { 39 | foreach ($setting_instances as $setting_instance) { 40 | $type = $setting_instance->get_setting_type(); 41 | 42 | if ("date_range" === $type || "date_single" === $type) { 43 | $setting_instance->enqueue_locale(); 44 | } 45 | } 46 | } 47 | 48 | /** 49 | * Get the clean props array for the given class instances. 50 | * 51 | * @since 1.0.0 52 | */ 53 | function get_props($instances = [], $post_type_current = "") 54 | { 55 | $props_array = []; 56 | 57 | foreach ($instances as $instance) { 58 | $post_type = $instance->get_post_type(); 59 | 60 | // Only push the props from the settings that belong to the current post type. 61 | if ( 62 | empty($post_type) || 63 | (is_array($post_type) && 64 | in_array($post_type_current, $post_type)) || 65 | $post_type_current === $post_type 66 | ) { 67 | $props_array[] = $instance->get_props_for_js(); 68 | } 69 | } 70 | 71 | return $props_array; 72 | } 73 | 74 | /** 75 | * Set meta_key_exists prop value. 76 | * 77 | * @since 1.0.0 78 | */ 79 | function set_meta_key_exists($setting_instances = [], $post_id = 0) 80 | { 81 | $post_id = !empty($post_id) ? $post_id : get_the_ID(); 82 | 83 | foreach ($setting_instances as $setting_instance) { 84 | $setting_instance->set_meta_key_exists($post_id); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /inc/core/core-create_instances.php: -------------------------------------------------------------------------------- 1 | [], 19 | "tabs" => [], 20 | "panels" => [], 21 | "settings" => [], 22 | ]; 23 | 24 | foreach ($sidebars_props as $sidebar_props) { 25 | if (empty($sidebar_props["tabs"])) { 26 | continue; 27 | } 28 | 29 | $sidebar = new Sidebar($sidebar_props); 30 | 31 | $instances["sidebars"][] = $sidebar; 32 | 33 | $sidebar_path = $sidebar->get_id(); 34 | 35 | $root_props = [ 36 | "data_key_prefix_from_sidebar" => $sidebar->get_data_key_prefix(), 37 | "id_prefix" => $sidebar->get_id_prefix(), 38 | "post_type" => $sidebar->get_post_type(), 39 | ]; 40 | 41 | foreach ($sidebar_props["tabs"] as $tab_props) { 42 | if (empty($tab_props["panels"])) { 43 | continue; 44 | } 45 | 46 | $tab_props = add_root_props($tab_props, $sidebar_path, $root_props); 47 | 48 | $tab = new Tab($tab_props); 49 | 50 | $instances["tabs"][] = $tab; 51 | 52 | $tab_path = [$sidebar_path, $tab->get_id()]; 53 | 54 | foreach ($tab_props["panels"] as $panel_props) { 55 | if (empty($panel_props["settings"])) { 56 | continue; 57 | } 58 | 59 | $panel_props = add_root_props( 60 | $panel_props, 61 | $tab_path, 62 | $root_props 63 | ); 64 | 65 | $panel = new Panel($panel_props); 66 | 67 | $instances["panels"][] = $panel; 68 | 69 | $panel_path = array_merge($tab_path, [$panel->get_id()]); 70 | 71 | foreach ($panel_props["settings"] as $setting_props) { 72 | $setting_props = add_root_props( 73 | $setting_props, 74 | $panel_path, 75 | $root_props, 76 | true 77 | ); 78 | 79 | $setting = create_setting_instance($setting_props); 80 | 81 | if (is_array($setting)) { 82 | foreach ($setting as $setting_ind) { 83 | if (!empty($setting_ind)) { 84 | $instances["settings"][] = $setting_ind; 85 | } 86 | } 87 | } elseif (!empty($setting)) { 88 | $instances["settings"][] = $setting; 89 | } 90 | } 91 | } 92 | } 93 | } 94 | 95 | return $instances; 96 | } 97 | 98 | /** 99 | * Add sidebar props to the props of a children element 100 | * 101 | * @since 1.1.0 102 | */ 103 | function add_root_props( 104 | $prop_raw = [], 105 | $path = "", 106 | $root_props = [], 107 | $add_data_key_prefix_from_sidebar = false 108 | ) { 109 | $prop_raw["path"] = $path; 110 | $prop_raw["id_prefix"] = $root_props["id_prefix"]; 111 | $prop_raw["post_type"] = $root_props["post_type"]; 112 | 113 | if (true === $add_data_key_prefix_from_sidebar) { 114 | $prop_raw["data_key_prefix_from_sidebar"] = 115 | $root_props["data_key_prefix_from_sidebar"]; 116 | } 117 | 118 | return $prop_raw; 119 | } 120 | 121 | /** 122 | * Create setting class instance 123 | * 124 | * @since 1.1.0 125 | */ 126 | function create_setting_instance($setting_props) 127 | { 128 | $setting = null; 129 | 130 | switch ($setting_props["type"]) { 131 | case "buttons": 132 | $setting = new Buttons($setting_props); 133 | break; 134 | 135 | case "checkbox": 136 | $setting = new Checkbox($setting_props); 137 | break; 138 | 139 | case "checkbox_multiple": 140 | $setting = new CheckboxMultiple($setting_props); 141 | break; 142 | 143 | case "color": 144 | $setting = new Color($setting_props); 145 | break; 146 | 147 | case "custom_text": 148 | $setting = new CustomText($setting_props); 149 | break; 150 | 151 | case "date_range": 152 | $setting = new DateRange($setting_props); 153 | break; 154 | 155 | case "date_single": 156 | $setting = new DateSingle($setting_props); 157 | break; 158 | 159 | case "image": 160 | $setting = new Image($setting_props); 161 | break; 162 | 163 | case "image_multiple": 164 | $setting = new ImageMultiple($setting_props); 165 | break; 166 | 167 | case "radio": 168 | $setting = new Radio($setting_props); 169 | break; 170 | 171 | case "range": 172 | $setting = new Range($setting_props); 173 | break; 174 | 175 | case "range_float": 176 | $setting = new RangeFloat($setting_props); 177 | break; 178 | 179 | case "select": 180 | $setting = new Select($setting_props); 181 | break; 182 | 183 | case "text": 184 | $setting = new Text($setting_props); 185 | break; 186 | 187 | case "textarea": 188 | $setting = new Textarea($setting_props); 189 | break; 190 | 191 | // Pro: 192 | case "custom_component": 193 | if (class_exists(__NAMESPACE__ . "\CustomComponent")) { 194 | $setting = new CustomComponent($setting_props); 195 | } 196 | break; 197 | 198 | case "custom_html": 199 | if (class_exists(__NAMESPACE__ . "\CustomHTML")) { 200 | $setting = new CustomHTML($setting_props); 201 | } 202 | break; 203 | 204 | case "repeatable": 205 | if (class_exists(__NAMESPACE__ . "\Repeatable")) { 206 | $setting = new Repeatable($setting_props); 207 | } 208 | break; 209 | 210 | default: 211 | break; 212 | } 213 | 214 | return $setting; 215 | } 216 | -------------------------------------------------------------------------------- /inc/core/core-create_sidebar.php: -------------------------------------------------------------------------------- 1 | get_params(); 16 | 17 | if (empty($data["post_id"])) { 18 | return false; 19 | } 20 | 21 | if (!current_user_can("edit_post", $data["post_id"])) { 22 | return false; 23 | } 24 | 25 | return true; 26 | } 27 | 28 | /** 29 | * Register a route to get the items data in the editor. 30 | */ 31 | add_action("rest_api_init", __NAMESPACE__ . '\register_route_items'); 32 | function register_route_items() 33 | { 34 | register_rest_route("post-meta-controls/v1", "/items", [ 35 | "methods" => "GET", 36 | "callback" => __NAMESPACE__ . "\get_items", 37 | "permission_callback" => __NAMESPACE__ . "\get_items_permission", 38 | ]); 39 | } 40 | function get_items($request) 41 | { 42 | $data = $request->get_params(); 43 | 44 | if (empty($data["post_id"]) || empty($data["post_type"])) { 45 | // Gutenberg throws the error invalid_json if null is sent. 46 | return false; 47 | } 48 | 49 | $post_id = $data["post_id"]; 50 | $post_type = $data["post_type"]; 51 | 52 | // Filter used to add custom sidebars inside other plugins/themes. 53 | $props_raw = apply_filters("pmc_create_sidebar", []); 54 | 55 | if (!is_array($props_raw)) { 56 | // Gutenberg throws the error invalid_json if null is sent. 57 | return false; 58 | } 59 | 60 | if (empty($props_raw)) { 61 | // Gutenberg throws the error invalid_json if null is sent. 62 | return false; 63 | } 64 | 65 | // Create the class instances for each item: sidebars, tabs, panels and settings. 66 | $instances = create_instances($props_raw); 67 | 68 | if ( 69 | empty($instances) || 70 | empty($instances["sidebars"]) || 71 | empty($instances["tabs"]) || 72 | empty($instances["panels"]) || 73 | empty($instances["settings"]) 74 | ) { 75 | // Gutenberg throws the error invalid_json if null is sent. 76 | return false; 77 | } 78 | 79 | // Set this property here, as the post id wasn't available before. 80 | set_meta_key_exists($instances["settings"], $post_id); 81 | 82 | // $post_type = get_post_type(); 83 | 84 | // Create an array of properties to localize in the main script. 85 | // It checks that the instance is assigned to the current post type. 86 | $props = [ 87 | "sidebars" => get_props($instances["sidebars"], $post_type), 88 | "tabs" => get_props($instances["tabs"], $post_type), 89 | "panels" => get_props($instances["panels"], $post_type), 90 | "settings" => get_props($instances["settings"], $post_type), 91 | ]; 92 | 93 | if ( 94 | empty($props["sidebars"]) || 95 | empty($props["tabs"]) || 96 | empty($props["panels"]) || 97 | empty($props["settings"]) 98 | ) { 99 | // Gutenberg throws the error invalid_json if null is sent. 100 | return false; 101 | } 102 | 103 | return $props; 104 | } 105 | -------------------------------------------------------------------------------- /inc/traits/trait-CastArray.php: -------------------------------------------------------------------------------- 1 | $value) { 21 | if (isset($schema[$key]["type"])) { 22 | $type = $schema[$key]["type"]; 23 | } elseif (isset($schema[$key])) { 24 | $type = $schema[$key]; 25 | } elseif (isset($schema["_all"])) { 26 | $type = $schema["_all"]; 27 | } else { 28 | unset($elements[$key]); 29 | continue; 30 | } 31 | 32 | if (is_array($type)) { 33 | $value = $this->cast_array($value); 34 | 35 | $elements[$key] = $this->cast_schema($value, $type); 36 | continue; 37 | } 38 | 39 | switch ($type) { 40 | case "html": 41 | $elements[$key] = $this->sanitize_html($value); 42 | break; 43 | 44 | case "html_svg": 45 | $elements[$key] = $this->sanitize_html_svg($value); 46 | break; 47 | 48 | case "html_raw": 49 | $elements[$key] = $this->sanitize_html_raw($value); 50 | break; 51 | 52 | case "id": 53 | $elements[$key] = $this->sanitize_id($value); 54 | break; 55 | 56 | case "text": 57 | $elements[$key] = $this->sanitize_text($value); 58 | break; 59 | 60 | case "textarea": 61 | $elements[$key] = $this->sanitize_textarea($value); 62 | break; 63 | 64 | case "float": 65 | $elements[$key] = $this->sanitize_float($value); 66 | break; 67 | 68 | case "integer": 69 | $elements[$key] = $this->sanitize_integer($value); 70 | break; 71 | 72 | case "boolean": 73 | $elements[$key] = $this->sanitize_boolean($value); 74 | break; 75 | 76 | default: 77 | $elements[$key] = ""; 78 | break; 79 | } 80 | } 81 | 82 | return $elements; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /inc/traits/trait-DateLocales.php: -------------------------------------------------------------------------------- 1 | get_meta_type($props); 39 | break; 40 | 41 | case "checkbox": 42 | return "boolean"; 43 | break; 44 | 45 | case "range_float": 46 | return "number"; 47 | break; 48 | 49 | case "image": 50 | case "image_multiple": 51 | case "range": 52 | return "integer"; 53 | break; 54 | 55 | default: 56 | return "string"; 57 | break; 58 | } 59 | } 60 | 61 | /** 62 | * Get the meta single property value given the setting type. 63 | */ 64 | protected function get_meta_single($setting_type = "") 65 | { 66 | if ( 67 | "repeatable" === $setting_type || 68 | "date_range" === $setting_type || 69 | "checkbox_multiple" === $setting_type || 70 | "image_multiple" === $setting_type 71 | ) { 72 | return false; 73 | } 74 | 75 | return true; 76 | } 77 | 78 | /** 79 | * Get the meta sanitize callback function. 80 | */ 81 | protected function get_meta_sanitize($props = []) 82 | { 83 | switch ($props["type"]) { 84 | case "repeatable": 85 | $props["type"] = $props["type_to_repeat"]; 86 | 87 | return $this->get_meta_sanitize($props); 88 | break; 89 | 90 | case "checkbox": 91 | return [$this, "sanitize_checkbox"]; 92 | break; 93 | 94 | case "textarea": 95 | return [$this, "sanitize_textarea"]; 96 | break; 97 | 98 | case "image": 99 | case "image_multiple": 100 | return function ($value) { 101 | return \absint($value); 102 | }; 103 | break; 104 | 105 | case "range": 106 | $min = $props["min"]; 107 | $max = $props["max"]; 108 | return function ($value) use ($min, $max) { 109 | return $this->sanitize_range($value, $min, $max); 110 | }; 111 | break; 112 | 113 | case "range_float": 114 | $min = $props["min"]; 115 | $max = $props["max"]; 116 | return function ($value) use ($min, $max) { 117 | return $this->sanitize_range_float($value, $min, $max); 118 | }; 119 | break; 120 | 121 | case "buttons": 122 | case "radio": 123 | case "select": 124 | case "checkbox_multiple": 125 | $options = $props["options"]; 126 | $default_value = 127 | "checkbox_multiple" === $props["type"] 128 | ? "" 129 | : $props["default_value"]; 130 | 131 | return function ($value) use ($options, $default_value) { 132 | return $this->sanitize_options( 133 | $value, 134 | $options, 135 | $default_value 136 | ); 137 | }; 138 | break; 139 | 140 | default: 141 | return [$this, "sanitize_text"]; 142 | break; 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /inc/traits/trait-PrepareOptions.php: -------------------------------------------------------------------------------- 1 | sanitize_array($options); 21 | 22 | $options_clean = []; 23 | 24 | foreach ($options as $key => $value) { 25 | if ( 26 | (!is_string($key) && !is_int($key)) || 27 | (!is_string($value) && !is_int($value)) 28 | ) { 29 | continue; 30 | } 31 | 32 | $options_clean[] = [ 33 | "value" => $this->sanitize_id($key), 34 | "label" => $this->sanitize_text($value), 35 | ]; 36 | } 37 | 38 | return $options_clean; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /inc/traits/trait-PreparePalette.php: -------------------------------------------------------------------------------- 1 | sanitize_array($palette); 22 | 23 | $palette_clean = []; 24 | 25 | foreach ($palette as $key => $value) { 26 | if (!is_string($key) || !is_string($value)) { 27 | continue; 28 | } 29 | 30 | $palette_clean[] = [ 31 | "name" => $key, 32 | "color" => $value, 33 | ]; 34 | } 35 | 36 | return $palette_clean; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /inc/traits/trait-ValidateConditions.php: -------------------------------------------------------------------------------- 1 | $schema) { 23 | if ( 24 | !isset($schema["conditions"]) || 25 | false === $schema["conditions"] 26 | ) { 27 | continue; 28 | } 29 | 30 | $conditions = $schema["conditions"]; 31 | 32 | if (is_array($conditions)) { 33 | foreach ($conditions as $condition) { 34 | $is_valid = $this->validate_condition( 35 | $condition, 36 | $props[$key] 37 | ); 38 | 39 | if (false === $is_valid) { 40 | return false; 41 | } 42 | } 43 | } else { 44 | $is_valid = $this->validate_condition( 45 | $conditions, 46 | $props[$key] 47 | ); 48 | } 49 | 50 | if (false === $is_valid) { 51 | return false; 52 | } 53 | } 54 | 55 | return true; 56 | } 57 | 58 | /** 59 | * Validate value based on given condition. 60 | */ 61 | private function validate_condition($condition, $value) 62 | { 63 | if ("not_empty" === $condition && empty($value)) { 64 | return false; 65 | } elseif (is_bool($condition) && false === $condition) { 66 | return false; 67 | } 68 | 69 | return true; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "post-meta-controls", 3 | "description": "Post Meta Controls", 4 | "version": "1.4.1", 5 | "homepage": "https://wordpress.org/plugins/post-meta-controls/", 6 | "author": "Alvaro Garcia", 7 | "private": true, 8 | "scripts": { 9 | "start": "npm run dev", 10 | "predev": "npm run clean:dist", 11 | "dev": "webpack --mode=development", 12 | "prebuild": "npm run clean:dist && npm run clean:release && npm run test", 13 | "build": "webpack --mode=production", 14 | "postbuild": "npm run replace:version && npm run copy:release", 15 | "clean:dist": "rm -rf dist", 16 | "clean:release": "rm -rf _release", 17 | "copy:release": "node scripts/copy-files.js", 18 | "replace:version": "node scripts/replace-version.js", 19 | "test": "npm run test:types && npm run test:lint && npm run test:prettier", 20 | "test:types": "tsc", 21 | "test:lint": "eslint \"**/*.{js,ts,tsx}\"", 22 | "test:prettier": "prettier --check \"**/*.{js,ts,tsx,php,json}\"", 23 | "prettier": "prettier --write \"**/*.{js,ts,tsx,php,json}\"" 24 | }, 25 | "dependencies": { 26 | "@babel/core": "^7.12.10", 27 | "@babel/plugin-proposal-class-properties": "^7.12.1", 28 | "@babel/preset-env": "^7.12.11", 29 | "@babel/preset-react": "^7.12.10", 30 | "@babel/preset-typescript": "^7.12.7", 31 | "@babel/register": "^7.12.10", 32 | "@types/dompurify": "2.2.1", 33 | "@types/lodash": "^4.14.168", 34 | "@types/react": "^16.14.2", 35 | "@types/react-dates": "^21.8.1", 36 | "@types/react-dom": "^16.9.10", 37 | "@types/tinycolor2": "^1.4.2", 38 | "@types/uuid": "^8.3.0", 39 | "@types/wordpress__api-fetch": "^3.2.3", 40 | "@types/wordpress__block-editor": "^2.2.9", 41 | "@types/wordpress__compose": "^3.4.3", 42 | "@types/wordpress__edit-post": "^3.5.4", 43 | "@types/wordpress__editor": "^9.4.5", 44 | "@types/wordpress__plugins": "^2.3.7", 45 | "@typescript-eslint/eslint-plugin": "^4.14.0", 46 | "@typescript-eslint/parser": "^4.14.0", 47 | "@wordpress/browserslist-config": "^3.0.0", 48 | "@wordpress/components": "^12.0.2", 49 | "@wordpress/data": "^4.26.2", 50 | "@wordpress/element": "^2.19.0", 51 | "@wordpress/hooks": "^2.11.0", 52 | "@wordpress/i18n": "^3.17.0", 53 | "@wordpress/url": "^2.21.1", 54 | "array-move": "^3.0.1", 55 | "babel-loader": "^8.2.2", 56 | "browserslist": "^4.16.1", 57 | "copyfiles": "^2.4.1", 58 | "css-loader": "^5.0.1", 59 | "css-minimizer-webpack-plugin": "^1.2.0", 60 | "dompurify": "^2.2.6", 61 | "immer": "^8.0.1", 62 | "mini-css-extract-plugin": "^1.3.4", 63 | "moment": "^2.29.1", 64 | "nib": "^1.1.2", 65 | "react": "^16.14.0", 66 | "react-dates": "^21.8.0", 67 | "react-dom": "^16.14.0", 68 | "react-sortable-hoc": "^1.11.0", 69 | "react-with-direction": "^1.3.1", 70 | "replace-in-file": "^6.1.0", 71 | "stylus": "^0.54.8", 72 | "stylus-loader": "^4.3.3", 73 | "terser-webpack-plugin": "^5.1.1", 74 | "tinycolor2": "^1.4.2", 75 | "typescript": "^4.1.3", 76 | "uuid": "^8.3.2", 77 | "webpack": "^5.17.0", 78 | "webpack-cli": "^4.4.0" 79 | }, 80 | "devDependencies": { 81 | "@prettier/plugin-php": "^0.16.1", 82 | "eslint": "^7.18.0", 83 | "eslint-config-prettier": "^7.2.0", 84 | "eslint-plugin-prettier": "^3.3.1", 85 | "eslint-plugin-react": "^7.22.0", 86 | "prettier": "^2.2.1" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /post-meta-controls.php: -------------------------------------------------------------------------------- 1 | null 10 | ); 11 | -------------------------------------------------------------------------------- /scripts/replace-version.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const replace = require("replace-in-file"); 3 | 4 | const { name, version } = require("../package.json"); 5 | 6 | // Replace the version number 7 | replace.sync({ 8 | files: path.resolve(__dirname, `../${name}.php`), 9 | from: [ 10 | /( \* Version: )\d+\.\d+\.\d+(-(beta|rc)(\d+)?)?/, 11 | /(define.*?PLUGIN_VERSION.*?)\d+\.\d+\.\d+(-(beta|rc)(\d+)?)?/, 12 | ], 13 | to: `$1${version}`, 14 | }); 15 | 16 | replace.sync({ 17 | files: path.resolve(__dirname, "../README.txt"), 18 | from: /(Stable tag: )\d+\.\d+\.\d+(-(beta|rc)(\d+)?)?/, 19 | to: `$1${version}`, 20 | }); 21 | -------------------------------------------------------------------------------- /scripts/webpack_loader-moment.js: -------------------------------------------------------------------------------- 1 | module.exports = source => { 2 | const sourceModified = source.replace( 3 | /^[\S\s]*?}\(this, \(([\S\s]*?)}\)\)\);/g, 4 | 5 | [ 6 | `window.wp.hooks.addAction( "postMetaControls.addMomentLocale", "addMomentLocale", `, 7 | `$1`, 8 | `});`, 9 | ].join("") 10 | ); 11 | 12 | return sourceModified; 13 | }; 14 | -------------------------------------------------------------------------------- /src/classes/Panel.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuid } from "uuid"; 2 | 3 | import { Base } from "./Base"; 4 | 5 | export interface Props { 6 | props: PanelProps; 7 | props_raw: PanelPropsRaw; 8 | props_privates: PanelPropsPrivates; 9 | props_defaults: PanelProps; 10 | props_schema: PanelPropsSchema; 11 | } 12 | 13 | export class Panel extends Base { 14 | constructor(props_raw: Props["props_raw"]) { 15 | super({ 16 | props_raw, 17 | 18 | props_privates: ["class_name", "warnings"], 19 | 20 | props_defaults: { 21 | class_name: "panel", 22 | warnings: [], 23 | id: uuid(), 24 | path: [], 25 | label: "", 26 | initial_open: false, 27 | collapsible: true, 28 | icon_dashicon: "", 29 | icon_svg: "", 30 | }, 31 | 32 | props_schema: { 33 | class_name: { 34 | type: "id", 35 | conditions: "not_empty", 36 | }, 37 | warnings: { 38 | type: { _all: { title: "text", message: "text" } }, 39 | }, 40 | id: { 41 | type: "id", 42 | conditions: "not_empty", 43 | }, 44 | path: { 45 | type: { _all: "id" }, 46 | conditions: "not_empty", 47 | }, 48 | label: { 49 | type: "text", 50 | conditions: 51 | props_raw.collapsible === true ? "not_empty" : false, 52 | }, 53 | initial_open: { 54 | type: "boolean", 55 | }, 56 | collapsible: { 57 | type: "boolean", 58 | }, 59 | icon_dashicon: { 60 | type: "id", 61 | }, 62 | icon_svg: { 63 | type: "html", 64 | }, 65 | }, 66 | }); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/classes/Setting.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuid } from "uuid"; 2 | 3 | import { Base } from "./Base"; 4 | 5 | export interface Props { 6 | props: SettingProps; 7 | props_raw: SettingPropsRaw; 8 | props_privates: SettingPropsPrivates; 9 | props_defaults: SettingProps; 10 | props_schema: SettingPropsSchema; 11 | } 12 | 13 | interface PropsReceived { 14 | props_raw: SettingPropsRawReceived; 15 | props_defaults: SettingPropsReceived; 16 | props_schema: SettingPropsSchemaReceived; 17 | } 18 | 19 | export class Setting extends Base { 20 | constructor({ props_raw, props_defaults, props_schema }: T) { 21 | super({ 22 | props_raw, 23 | 24 | props_privates: ["class_name", "warnings"], 25 | 26 | props_defaults: { 27 | ...props_defaults, 28 | class_name: "setting", 29 | warnings: [], 30 | id: uuid(), 31 | path: [], 32 | label: "", 33 | help: "", 34 | data_type: "none", 35 | meta_key_exists: false, 36 | data_key_with_prefix: "", 37 | ui_border_top: true, 38 | }, 39 | 40 | props_schema: { 41 | ...props_schema, 42 | class_name: { 43 | type: "id", 44 | conditions: "not_empty", 45 | }, 46 | warnings: { 47 | type: { _all: { title: "text", message: "text" } }, 48 | }, 49 | id: { 50 | type: "id", 51 | conditions: "not_empty", 52 | }, 53 | path: { 54 | type: { _all: "id" }, 55 | conditions: "not_empty", 56 | }, 57 | label: { 58 | type: "text", 59 | }, 60 | type: { 61 | type: "id", 62 | conditions: "not_empty", 63 | }, 64 | help: { 65 | type: "text", 66 | }, 67 | data_type: { 68 | type: "id", 69 | }, 70 | meta_key_exists: { 71 | type: "boolean", 72 | }, 73 | data_key_with_prefix: { 74 | type: "id", 75 | conditions: 76 | props_raw.data_type !== "none" ? "not_empty" : false, 77 | }, 78 | ui_border_top: { 79 | type: "boolean", 80 | }, 81 | }, 82 | }); 83 | } 84 | 85 | beforeSetSchema = (props_raw: SettingProps): SettingProps => 86 | this.prepareDataType(props_raw); 87 | 88 | prepareDataType = (props_raw: SettingProps): SettingProps => { 89 | const { data_type, type } = props_raw; 90 | const types_can_have_meta = [ 91 | "buttons", 92 | "checkbox", 93 | "checkbox_multiple", 94 | "color", 95 | "date_range", 96 | "date_single", 97 | "image", 98 | "image_multiple", 99 | "repeatable", 100 | "radio", 101 | "range", 102 | "range_float", 103 | "select", 104 | "text", 105 | "textarea", 106 | ]; 107 | 108 | if (data_type === "meta" && types_can_have_meta.includes(type)) { 109 | return props_raw; 110 | } 111 | 112 | const types_can_have_localstorage = [ 113 | "buttons", 114 | "checkbox", 115 | "checkbox_multiple", 116 | "color", 117 | "date_range", 118 | "date_single", 119 | "image", 120 | "image_multiple", 121 | "repeatable", 122 | "radio", 123 | "range", 124 | "range_float", 125 | "select", 126 | ]; 127 | 128 | if ( 129 | data_type === "localstorage" && 130 | types_can_have_localstorage.includes(type) 131 | ) { 132 | return props_raw; 133 | } 134 | 135 | return { 136 | ...props_raw, 137 | data_type: "none", 138 | data_key_with_prefix: "", 139 | }; 140 | }; 141 | 142 | getDataType = (): SettingProps["data_type"] => this.props.data_type; 143 | 144 | getDataKey = (): SettingProps["data_key_with_prefix"] => 145 | this.props.data_key_with_prefix; 146 | } 147 | -------------------------------------------------------------------------------- /src/classes/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import DOMPurify from "dompurify"; 3 | import { v4 as uuid } from "uuid"; 4 | import { registerPlugin } from "@wordpress/plugins"; 5 | import { RawHTML } from "@wordpress/element"; 6 | 7 | import { addPrefix } from "@/utils/tools"; 8 | import { App } from "@/components/App/App"; 9 | import { Base } from "./Base"; 10 | 11 | export interface Props { 12 | props: SidebarProps; 13 | props_raw: SidebarPropsRaw; 14 | props_privates: SidebarPropsPrivates; 15 | props_defaults: SidebarProps; 16 | props_schema: SidebarPropsSchema; 17 | } 18 | 19 | export class Sidebar extends Base { 20 | constructor(props_raw: Props["props_raw"]) { 21 | super({ 22 | props_raw, 23 | 24 | props_privates: ["class_name", "warnings"], 25 | 26 | props_defaults: { 27 | class_name: "sidebar", 28 | warnings: [], 29 | id: uuid(), 30 | label: "", 31 | active_tab: "", 32 | settings_id: [], 33 | icon_dashicon: "carrot", 34 | icon_svg: "", 35 | id_already_exists: false, 36 | ui_color_scheme: "light", 37 | }, 38 | 39 | props_schema: { 40 | class_name: { 41 | type: "id", 42 | conditions: "not_empty", 43 | }, 44 | warnings: { 45 | type: { _all: { title: "text", message: "text" } }, 46 | }, 47 | id: { 48 | type: "id", 49 | conditions: "not_empty", 50 | }, 51 | label: { 52 | type: "text", 53 | conditions: "not_empty", 54 | }, 55 | icon_dashicon: { 56 | type: "id", 57 | }, 58 | icon_svg: { 59 | type: "html", 60 | }, 61 | active_tab: { 62 | type: "id", 63 | }, 64 | settings_id: { 65 | type: { _all: "integer" }, 66 | }, 67 | id_already_exists: { 68 | type: "boolean", 69 | }, 70 | ui_color_scheme: { 71 | type: "id", 72 | }, 73 | }, 74 | }); 75 | } 76 | 77 | registerPlugin = (): void => { 78 | const { 79 | id, 80 | icon_dashicon, 81 | icon_svg, 82 | id_already_exists, 83 | label, 84 | } = this.props; 85 | 86 | let plugin_id: string; 87 | 88 | if (id_already_exists) { 89 | // If it is not valid we still register the plugin sidebar 90 | // with a different id. This way we can include the warnings. 91 | plugin_id = addPrefix(uuid()); 92 | } else { 93 | plugin_id = addPrefix(id); 94 | plugin_id = plugin_id.replace(/_/g, "-"); 95 | plugin_id = plugin_id.replace(/[^a-zA-Z0-9-]/g, ""); 96 | } 97 | 98 | const icon = 99 | icon_svg !== "" ? ( 100 | 101 | {DOMPurify.sanitize(icon_svg)} 102 | 103 | ) : ( 104 | DOMPurify.sanitize(icon_dashicon) 105 | ); 106 | 107 | registerPlugin(plugin_id, { 108 | // @ts-expect-error TODO 109 | icon: icon || "carrot", 110 | render: () => ( 111 | 112 | ), 113 | }); 114 | }; 115 | 116 | setIdAlreadyExists = (): void => { 117 | this.props.id_already_exists = true; 118 | }; 119 | } 120 | -------------------------------------------------------------------------------- /src/classes/Tab.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuid } from "uuid"; 2 | 3 | import { Base } from "./Base"; 4 | 5 | export interface Props { 6 | props: TabProps; 7 | props_raw: TabPropsRaw; 8 | props_privates: TabPropsPrivates; 9 | props_defaults: TabProps; 10 | props_schema: TabPropsSchema; 11 | } 12 | 13 | export class Tab extends Base { 14 | constructor(props_raw: Props["props_raw"]) { 15 | super({ 16 | props_raw, 17 | 18 | props_privates: ["class_name", "warnings"], 19 | 20 | props_defaults: { 21 | class_name: "tab", 22 | warnings: [], 23 | id: uuid(), 24 | path: [], 25 | label: "", 26 | icon_dashicon: "", 27 | icon_svg: "", 28 | }, 29 | 30 | props_schema: { 31 | class_name: { 32 | type: "id", 33 | conditions: "not_empty", 34 | }, 35 | warnings: { 36 | type: { _all: { title: "text", message: "text" } }, 37 | }, 38 | id: { 39 | type: "id", 40 | conditions: "not_empty", 41 | }, 42 | path: { 43 | type: { _all: "id" }, 44 | conditions: "not_empty", 45 | }, 46 | label: { 47 | type: "text", 48 | conditions: "not_empty", 49 | }, 50 | icon_dashicon: { 51 | type: "id", 52 | }, 53 | icon_svg: { 54 | type: "html", 55 | }, 56 | }, 57 | }); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/classes/index.ts: -------------------------------------------------------------------------------- 1 | export { Sidebar } from "./Sidebar"; 2 | export { Tab } from "./Tab"; 3 | export { Panel } from "./Panel"; 4 | export { Setting } from "./Setting"; 5 | 6 | // Settings 7 | export { Buttons } from "./settings/Buttons"; 8 | export { Checkbox } from "./settings/Checkbox"; 9 | export { CheckboxMultiple } from "./settings/CheckboxMultiple"; 10 | export { Color } from "./settings/Color"; 11 | export { CustomText } from "./settings/CustomText"; 12 | export { DateRange } from "./settings/DateRange"; 13 | export { DateSingle } from "./settings/DateSingle"; 14 | export { Image } from "./settings/Image"; 15 | export { ImageMultiple } from "./settings/ImageMultiple"; 16 | export { Radio } from "./settings/Radio"; 17 | export { Range } from "./settings/Range"; 18 | export { RangeFloat } from "./settings/RangeFloat"; 19 | export { Select } from "./settings/Select"; 20 | export { Text } from "./settings/Text"; 21 | export { Textarea } from "./settings/Textarea"; 22 | -------------------------------------------------------------------------------- /src/classes/settings/Buttons.ts: -------------------------------------------------------------------------------- 1 | import { Setting } from "../Setting"; 2 | 3 | interface Props { 4 | props_raw: ButtonsPropsRaw & 5 | Omit; 6 | props_defaults: ButtonsProps; 7 | props_schema: ButtonsPropsSchema; 8 | } 9 | 10 | export class Buttons extends Setting { 11 | constructor(props_raw: Props["props_raw"]) { 12 | super({ 13 | props_raw, 14 | 15 | props_defaults: { 16 | type: "buttons", 17 | default_value: "", 18 | allow_empty: false, 19 | options: [], 20 | }, 21 | 22 | props_schema: { 23 | default_value: { 24 | type: "id", 25 | }, 26 | allow_empty: { 27 | type: "boolean", 28 | }, 29 | options: { 30 | type: { 31 | _all: { 32 | value: "id", 33 | title: "text", 34 | icon_dashicon: "id", 35 | icon_svg: "html", 36 | }, 37 | }, 38 | conditions: "not_empty", 39 | }, 40 | }, 41 | }); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/classes/settings/Checkbox.ts: -------------------------------------------------------------------------------- 1 | import { Setting } from "../Setting"; 2 | 3 | interface Props { 4 | props_raw: CheckboxPropsRaw & 5 | Omit; 6 | props_defaults: CheckboxProps; 7 | props_schema: CheckboxPropsSchema; 8 | } 9 | 10 | export class Checkbox extends Setting { 11 | constructor(props_raw: Props["props_raw"]) { 12 | super({ 13 | props_raw, 14 | 15 | props_defaults: { 16 | type: "checkbox", 17 | default_value: false, 18 | input_label: "", 19 | use_toggle: false, 20 | }, 21 | 22 | props_schema: { 23 | default_value: { 24 | type: "boolean", 25 | }, 26 | input_label: { 27 | type: "text", 28 | conditions: "not_empty", 29 | }, 30 | use_toggle: { 31 | type: "boolean", 32 | }, 33 | }, 34 | }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/classes/settings/CheckboxMultiple.ts: -------------------------------------------------------------------------------- 1 | import { prepareOptions } from "@/utils/tools"; 2 | import { Setting } from "../Setting"; 3 | 4 | interface Props { 5 | props_raw: CheckboxMultiplePropsRaw & 6 | Omit; 7 | props_defaults: CheckboxMultipleProps; 8 | props_schema: CheckboxMultiplePropsSchema; 9 | } 10 | 11 | export class CheckboxMultiple extends Setting { 12 | constructor(props_raw: Props["props_raw"]) { 13 | super({ 14 | props_raw, 15 | 16 | props_defaults: { 17 | type: "checkbox_multiple", 18 | default_value: [], 19 | options: [], 20 | use_toggle: false, 21 | }, 22 | 23 | props_schema: { 24 | default_value: { 25 | type: { _all: "id" }, 26 | }, 27 | options: { 28 | type: { _all: { value: "id", label: "text" } }, 29 | conditions: "not_empty", 30 | }, 31 | use_toggle: { 32 | type: "boolean", 33 | }, 34 | }, 35 | }); 36 | } 37 | 38 | beforeSetSchema = (props_raw: SettingProps): SettingProps => { 39 | props_raw = super.beforeSetSchema(props_raw); 40 | 41 | // @ts-expect-error TODO 42 | props_raw.options = prepareOptions(props_raw.options); 43 | 44 | return props_raw; 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /src/classes/settings/Color.ts: -------------------------------------------------------------------------------- 1 | import { isUndefined, reduce, isArray } from "lodash"; 2 | 3 | import { Setting } from "../Setting"; 4 | 5 | interface Props { 6 | props_raw: ColorPropsRaw & 7 | Omit; 8 | props_defaults: ColorProps; 9 | props_schema: ColorPropsSchema; 10 | } 11 | 12 | export class Color extends Setting { 13 | constructor(props_raw: Props["props_raw"]) { 14 | super({ 15 | props_raw, 16 | 17 | props_defaults: { 18 | type: "color", 19 | default_value: "", 20 | alpha_control: false, 21 | palette: [], 22 | }, 23 | 24 | props_schema: { 25 | default_value: { type: "text" }, 26 | alpha_control: { type: "boolean" }, 27 | palette: { type: { _all: { name: "id", color: "text" } } }, 28 | }, 29 | }); 30 | } 31 | 32 | beforeSetSchema = (props_raw: SettingProps): SettingProps => { 33 | props_raw = super.beforeSetSchema(props_raw); 34 | 35 | // @ts-expect-error TODO 36 | props_raw.palette = this.preparePalette(props_raw.palette); 37 | 38 | return props_raw; 39 | }; 40 | 41 | preparePalette( 42 | palette: ColorPaletteRaw | ColorPalette[] | undefined 43 | ): ColorPalette[] | undefined { 44 | if (isArray(palette) || isUndefined(palette)) { 45 | return palette; 46 | } 47 | 48 | return reduce( 49 | palette, 50 | (acc, value, key) => 51 | acc.concat({ 52 | name: key, 53 | color: value, 54 | }), 55 | [] 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/classes/settings/CustomText.ts: -------------------------------------------------------------------------------- 1 | import { Setting } from "../Setting"; 2 | 3 | interface Props { 4 | props_raw: CustomTextPropsRaw & 5 | Omit; 6 | props_defaults: CustomTextProps; 7 | props_schema: CustomTextPropsSchema; 8 | } 9 | 10 | export class CustomText extends Setting { 11 | constructor(props_raw: Props["props_raw"]) { 12 | super({ 13 | props_raw, 14 | 15 | props_defaults: { 16 | type: "custom_text", 17 | content: [], 18 | }, 19 | 20 | props_schema: { 21 | content: { 22 | type: { 23 | _all: { 24 | type: "id", 25 | content: { _all: "text" }, 26 | href: "url", 27 | }, 28 | }, 29 | conditions: "not_empty", 30 | }, 31 | }, 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/classes/settings/DateRange.ts: -------------------------------------------------------------------------------- 1 | import { Setting } from "../Setting"; 2 | 3 | interface Props { 4 | props_raw: DateRangePropsRaw & 5 | Omit; 6 | props_defaults: DateRangeProps; 7 | props_schema: DateRangePropsSchema; 8 | } 9 | 10 | export class DateRange extends Setting { 11 | constructor(props_raw: Props["props_raw"]) { 12 | super({ 13 | props_raw, 14 | 15 | props_defaults: { 16 | type: "date_range", 17 | default_value: [], 18 | format: "DD/MM/YYYY", 19 | locale: "en", 20 | unavailable_dates: [["before", "today"]], 21 | minimum_days: 1, 22 | maximum_days: 0, 23 | }, 24 | 25 | props_schema: { 26 | default_value: { 27 | type: { _all: "text" }, 28 | }, 29 | format: { 30 | type: "text", 31 | }, 32 | locale: { 33 | type: "id", 34 | }, 35 | unavailable_dates: { 36 | type: { _all: { _all: "text" } }, 37 | }, 38 | minimum_days: { 39 | type: "integer", 40 | }, 41 | maximum_days: { 42 | type: "integer", 43 | }, 44 | }, 45 | }); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/classes/settings/DateSingle.ts: -------------------------------------------------------------------------------- 1 | import { Setting } from "../Setting"; 2 | 3 | interface Props { 4 | props_raw: DateSinglePropsRaw & 5 | Omit; 6 | props_defaults: DateSingleProps; 7 | props_schema: DateSinglePropsSchema; 8 | } 9 | 10 | export class DateSingle extends Setting { 11 | constructor(props_raw: Props["props_raw"]) { 12 | super({ 13 | props_raw, 14 | 15 | props_defaults: { 16 | type: "date_single", 17 | default_value: "", 18 | format: "DD/MM/YYYY", 19 | locale: "en", 20 | unavailable_dates: [["before", "today"]], 21 | }, 22 | 23 | props_schema: { 24 | default_value: { 25 | type: "text", 26 | }, 27 | format: { 28 | type: "text", 29 | }, 30 | locale: { 31 | type: "id", 32 | }, 33 | unavailable_dates: { 34 | type: { _all: { _all: "text" } }, 35 | }, 36 | }, 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/classes/settings/Image.ts: -------------------------------------------------------------------------------- 1 | import { Setting } from "../Setting"; 2 | 3 | interface Props { 4 | props_raw: ImagePropsRaw & 5 | Omit; 6 | props_defaults: ImageProps; 7 | props_schema: ImagePropsSchema; 8 | } 9 | 10 | export class Image extends Setting { 11 | constructor(props_raw: Props["props_raw"]) { 12 | super({ 13 | props_raw, 14 | 15 | props_defaults: { 16 | type: "image", 17 | default_value: 0, 18 | }, 19 | 20 | props_schema: { 21 | default_value: { 22 | type: "integer", 23 | }, 24 | }, 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/classes/settings/ImageMultiple.ts: -------------------------------------------------------------------------------- 1 | import { Setting } from "../Setting"; 2 | 3 | interface Props { 4 | props_raw: ImageMultiplePropsRaw & 5 | Omit; 6 | props_defaults: ImageMultipleProps; 7 | props_schema: ImageMultiplePropsSchema; 8 | } 9 | 10 | export class ImageMultiple extends Setting { 11 | constructor(props_raw: Props["props_raw"]) { 12 | super({ 13 | props_raw, 14 | 15 | props_defaults: { 16 | type: "image_multiple", 17 | default_value: [], 18 | }, 19 | 20 | props_schema: { 21 | default_value: { 22 | type: { _all: "integer" }, 23 | }, 24 | }, 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/classes/settings/Radio.ts: -------------------------------------------------------------------------------- 1 | import { prepareOptions } from "@/utils/tools"; 2 | import { Setting } from "../Setting"; 3 | 4 | interface Props { 5 | props_raw: RadioPropsRaw & 6 | Omit; 7 | props_defaults: RadioProps; 8 | props_schema: RadioPropsSchema; 9 | } 10 | 11 | export class Radio extends Setting { 12 | constructor(props_raw: Props["props_raw"]) { 13 | super({ 14 | props_raw, 15 | 16 | props_defaults: { 17 | type: "radio", 18 | default_value: "", 19 | options: [], 20 | }, 21 | 22 | props_schema: { 23 | default_value: { 24 | type: "id", 25 | }, 26 | options: { 27 | type: { _all: { value: "id", label: "text" } }, 28 | conditions: "not_empty", 29 | }, 30 | }, 31 | }); 32 | } 33 | 34 | beforeSetSchema = (props_raw: SettingProps): SettingProps => { 35 | props_raw = super.beforeSetSchema(props_raw); 36 | 37 | // @ts-expect-error TODO 38 | props_raw.options = prepareOptions(props_raw.options); 39 | 40 | return props_raw; 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /src/classes/settings/Range.ts: -------------------------------------------------------------------------------- 1 | import { isUndefined } from "lodash"; 2 | import { __, sprintf } from "@wordpress/i18n"; 3 | 4 | import { Setting } from "../Setting"; 5 | 6 | interface Props { 7 | props_raw: RangePropsRaw & 8 | Omit; 9 | props_defaults: RangeProps; 10 | props_schema: RangePropsSchema; 11 | } 12 | 13 | export class Range extends Setting { 14 | constructor(props_raw: Props["props_raw"]) { 15 | super({ 16 | props_raw, 17 | 18 | props_defaults: { 19 | type: "range", 20 | default_value: 0, 21 | step: 1, 22 | min: 0, 23 | max: 100, 24 | }, 25 | 26 | props_schema: { 27 | default_value: { 28 | type: "integer", 29 | }, 30 | step: { 31 | type: "integer", 32 | conditions: [ 33 | { 34 | value: !!props_raw.step && props_raw.step > 0, 35 | message: __("This value has to be greater than 0."), 36 | }, 37 | { 38 | value: 39 | !isUndefined(props_raw.max) && 40 | !isUndefined(props_raw.min) && 41 | !isUndefined(props_raw.step) && 42 | props_raw.max - props_raw.min > props_raw.step, 43 | /* translators: %s: max property, %s: min property. */ 44 | message: sprintf( 45 | __( 46 | "This value has to be greater than '%s' minus '%s' values." 47 | ), 48 | "max", 49 | "min" 50 | ), 51 | }, 52 | ], 53 | }, 54 | min: { 55 | type: "integer", 56 | }, 57 | max: { 58 | type: "integer", 59 | conditions: [ 60 | { 61 | value: 62 | !isUndefined(props_raw.max) && 63 | !isUndefined(props_raw.min) && 64 | props_raw.max > props_raw.min, 65 | /* translators: %s: min property. */ 66 | message: sprintf( 67 | __("This value has to be greater than '%s'."), 68 | "min" 69 | ), 70 | }, 71 | ], 72 | }, 73 | }, 74 | }); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/classes/settings/RangeFloat.ts: -------------------------------------------------------------------------------- 1 | import { isUndefined } from "lodash"; 2 | import { __, sprintf } from "@wordpress/i18n"; 3 | 4 | import { Setting } from "../Setting"; 5 | 6 | interface Props { 7 | props_raw: RangeFloatPropsRaw & 8 | Omit; 9 | props_defaults: RangeFloatProps; 10 | props_schema: RangeFloatPropsSchema; 11 | } 12 | 13 | export class RangeFloat extends Setting { 14 | constructor(props_raw: Props["props_raw"]) { 15 | super({ 16 | props_raw, 17 | 18 | props_defaults: { 19 | type: "range_float", 20 | default_value: 0, 21 | step: 1, 22 | min: 0, 23 | max: 100, 24 | }, 25 | 26 | props_schema: { 27 | default_value: { 28 | type: "float", 29 | }, 30 | step: { 31 | type: "float", 32 | conditions: [ 33 | { 34 | value: 35 | !isUndefined(props_raw.step) && 36 | props_raw.step > 0, 37 | message: __("This value has to be greater than 0."), 38 | }, 39 | { 40 | value: 41 | !isUndefined(props_raw.max) && 42 | !isUndefined(props_raw.min) && 43 | !isUndefined(props_raw.step) && 44 | props_raw.max - props_raw.min > props_raw.step, 45 | /* translators: %s: max property, %s: min property. */ 46 | message: sprintf( 47 | __( 48 | "This value has to be greater than '%s' minus '%s' values." 49 | ), 50 | "max", 51 | "min" 52 | ), 53 | }, 54 | ], 55 | }, 56 | min: { 57 | type: "float", 58 | }, 59 | max: { 60 | type: "float", 61 | conditions: [ 62 | { 63 | value: 64 | !isUndefined(props_raw.max) && 65 | !isUndefined(props_raw.min) && 66 | props_raw.max > props_raw.min, 67 | /* translators: %s: min property. */ 68 | message: sprintf( 69 | __("This value has to be greater than '%s'."), 70 | "min" 71 | ), 72 | }, 73 | ], 74 | }, 75 | }, 76 | }); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/classes/settings/Select.ts: -------------------------------------------------------------------------------- 1 | import { prepareOptions } from "@/utils/tools"; 2 | import { Setting } from "../Setting"; 3 | 4 | interface Props { 5 | props_raw: SelectPropsRaw & 6 | Omit; 7 | props_defaults: SelectProps; 8 | props_schema: SelectPropsSchema; 9 | } 10 | 11 | export class Select extends Setting { 12 | constructor(props_raw: Props["props_raw"]) { 13 | super({ 14 | props_raw, 15 | 16 | props_defaults: { 17 | type: "select", 18 | default_value: "", 19 | options: [], 20 | }, 21 | 22 | props_schema: { 23 | default_value: { 24 | type: "id", 25 | }, 26 | options: { 27 | type: { _all: { value: "id", label: "text" } }, 28 | conditions: "not_empty", 29 | }, 30 | }, 31 | }); 32 | } 33 | 34 | beforeSetSchema = (props_raw: SettingProps): SettingProps => { 35 | props_raw = super.beforeSetSchema(props_raw); 36 | 37 | // @ts-expect-error TODO 38 | props_raw.options = prepareOptions(props_raw.options); 39 | 40 | return props_raw; 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /src/classes/settings/Text.ts: -------------------------------------------------------------------------------- 1 | import { Setting } from "../Setting"; 2 | 3 | interface Props { 4 | props_raw: TextPropsRaw & 5 | Omit; 6 | props_defaults: TextProps; 7 | props_schema: TextPropsSchema; 8 | } 9 | 10 | export class Text extends Setting { 11 | constructor(props_raw: Props["props_raw"]) { 12 | super({ 13 | props_raw, 14 | 15 | props_defaults: { 16 | type: "text", 17 | default_value: "", 18 | placeholder: "", 19 | }, 20 | 21 | props_schema: { 22 | default_value: { 23 | type: "text", 24 | }, 25 | placeholder: { 26 | type: "text", 27 | }, 28 | }, 29 | }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/classes/settings/Textarea.ts: -------------------------------------------------------------------------------- 1 | import { Setting } from "../Setting"; 2 | 3 | interface Props { 4 | props_raw: TextareaPropsRaw & 5 | Omit; 6 | props_defaults: TextareaProps; 7 | props_schema: TextareaPropsSchema; 8 | } 9 | 10 | export class Textarea extends Setting { 11 | constructor(props_raw: Props["props_raw"]) { 12 | super({ 13 | props_raw, 14 | 15 | props_defaults: { 16 | type: "textarea", 17 | default_value: "", 18 | placeholder: "", 19 | }, 20 | 21 | props_schema: { 22 | default_value: { 23 | type: "text", 24 | }, 25 | placeholder: { 26 | type: "text", 27 | }, 28 | }, 29 | }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/components/App/App.styl: -------------------------------------------------------------------------------- 1 | /** 2 | * App 3 | */ 4 | 5 | @import "_color-fixes.styl" 6 | @import "_color-schemes.styl" 7 | @import "_logo-fixes.styl" 8 | -------------------------------------------------------------------------------- /src/components/App/App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Fragment } from "@wordpress/element"; 3 | import { PluginSidebar, PluginSidebarMoreMenuItem } from "@wordpress/edit-post"; 4 | 5 | import "./App.styl"; 6 | import { Sidebar } from "@/components/Sidebar"; 7 | 8 | interface Props { 9 | plugin_id: string; 10 | sidebar_id: SidebarProps["id"]; 11 | label: SidebarProps["label"]; 12 | } 13 | 14 | export const App: React.ComponentType = props => { 15 | const { plugin_id, sidebar_id, label } = props; 16 | 17 | return ( 18 | 19 | 20 | 21 | 22 | 23 | 24 | {label} 25 | 26 | 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /src/components/App/_color-schemes.styl: -------------------------------------------------------------------------------- 1 | /** 2 | * App - Color schemes 3 | */ 4 | 5 | +prefix-classes('pmc-') 6 | 7 | // Light 8 | .color_scheme-type-light 9 | --text_color rgba(0,0,0,0.87) 10 | --text_color_softer rgba(0,0,0,.75) 11 | --border_color rgba(0,0,0,.1) 12 | --border_color_softer rgba(0,0,0,.05) 13 | --border_color_stronger rgba(0,0,0,.4) 14 | --background_color #fff 15 | --background_color_softer rgba(255,255,255,.5) 16 | 17 | // plain_light 18 | --color_primary #666666 19 | --color_primary_softer #999 20 | -------------------------------------------------------------------------------- /src/components/App/_logo-fixes.styl: -------------------------------------------------------------------------------- 1 | /** 2 | * App - Logo fixes 3 | */ 4 | 5 | .edit-post-pinned-plugins .components-icon-button:not(.is-toggled) svg.dashicons-carrot path 6 | stroke none 7 | 8 | .edit-post-pinned-plugins .components-icon-button:not(.is-toggled) svg path[fill="none"] 9 | fill none 10 | stroke none 11 | 12 | // Override Gutenberg style (where !important is being used) 13 | .edit-post-pinned-plugins .components-icon-button:hover svg path[fill="none"] 14 | .edit-post-pinned-plugins .components-icon-button.is-toggled svg path[fill="none"] 15 | fill none!important 16 | stroke none!important 17 | 18 | .edit-post-pinned-plugins .components-icon-button:hover svg.dashicons-carrot path 19 | .edit-post-pinned-plugins .components-icon-button.is-toggled svg.dashicons-carrot path 20 | stroke none!important 21 | -------------------------------------------------------------------------------- /src/components/App/index.ts: -------------------------------------------------------------------------------- 1 | export { App } from "./App"; 2 | -------------------------------------------------------------------------------- /src/components/Buttons/Buttons.styl: -------------------------------------------------------------------------------- 1 | /** 2 | * Buttons 3 | */ 4 | 5 | .pmc-setting-buttons 6 | 7 | button span:empty 8 | display none 9 | 10 | .components-base-control__field 11 | flex-direction column 12 | display flex 13 | 14 | .components-toolbar 15 | min-height 0 16 | 17 | .components-toolbar__control.components-button:not(:disabled).is-active svg 18 | .components-toolbar__control.components-button:not(:disabled).is-pressed svg 19 | .components-toolbar__control.components-button:not(:disabled).is-pressed span 20 | outline none 21 | box-shadow none 22 | 23 | .components-toolbar__control.components-button svg 24 | .components-toolbar__control.components-button span 25 | padding 5px 26 | border-radius 2px 27 | height 30px 28 | width 30px 29 | 30 | .components-toolbar__control.components-button 31 | display inline-flex 32 | align-items flex-end 33 | margin 0 34 | outline none 35 | cursor pointer 36 | position relative 37 | width 36px 38 | height 36px 39 | box-shadow none 40 | 41 | &:before 42 | content normal 43 | 44 | #editor & 45 | padding 3px 46 | min-width 0 47 | min-height 0 48 | 49 | > div 50 | display flex 51 | 52 | .components-icon-button:not(:disabled):not([aria-disabled=true]):not(.is-default):hover 53 | box-shadow none 54 | -------------------------------------------------------------------------------- /src/components/Buttons/Buttons.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import DOMPurify from "dompurify"; 3 | import { RawHTML, Component } from "@wordpress/element"; 4 | import { Toolbar, BaseControl, ToolbarButton } from "@wordpress/components"; 5 | import { withState } from "@wordpress/compose"; 6 | 7 | import "./Buttons.styl"; 8 | 9 | interface WithStateProps { 10 | setState: SetState<{ icons: (string | JSX.Element)[] }>; 11 | icons: React.ReactNode[]; 12 | } 13 | 14 | interface OwnProps extends ButtonsProps, SettingPropsShared { 15 | updateValue: (value: string) => void; 16 | value: ButtonsProps["default_value"]; 17 | } 18 | 19 | interface Props extends OwnProps, WithStateProps {} 20 | 21 | export const Buttons: React.ComponentType = withState({ 22 | icons: null, 23 | })( 24 | class extends Component { 25 | componentDidMount() { 26 | const { options, setState } = this.props; 27 | 28 | setState({ 29 | icons: options.map(({ icon_svg, icon_dashicon }) => 30 | icon_svg ? ( 31 | {DOMPurify.sanitize(icon_svg)} 32 | ) : ( 33 | icon_dashicon 34 | ) 35 | ), 36 | }); 37 | } 38 | 39 | render() { 40 | const { 41 | id, 42 | label, 43 | help, 44 | options, 45 | value, 46 | allow_empty, 47 | updateValue, 48 | icons, 49 | } = this.props; 50 | 51 | if (!icons) { 52 | return null; 53 | } 54 | 55 | const controls = options.map( 56 | ({ title, value: option_value }, index) => ({ 57 | // @ts-expect-error TODO 58 | icon: icons[index], 59 | label: title, 60 | isActive: option_value === value, 61 | onClick: () => { 62 | if (allow_empty && option_value === value) { 63 | updateValue(""); 64 | } else { 65 | updateValue(option_value); 66 | } 67 | }, 68 | }) 69 | ); 70 | 71 | return ( 72 | 73 | 74 | 75 | ); 76 | } 77 | } 78 | ); 79 | -------------------------------------------------------------------------------- /src/components/Buttons/index.ts: -------------------------------------------------------------------------------- 1 | export { Buttons } from "./Buttons"; 2 | -------------------------------------------------------------------------------- /src/components/Checkbox/Checkbox.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | BaseControl, 4 | CheckboxControl, 5 | ToggleControl, 6 | } from "@wordpress/components"; 7 | 8 | interface Props extends CheckboxProps, SettingPropsShared { 9 | updateValue: (value: boolean) => void; 10 | value: CheckboxProps["default_value"]; 11 | } 12 | 13 | export const Checkbox: React.ComponentType = props => { 14 | const { 15 | id, 16 | input_label, 17 | label, 18 | help, 19 | value, 20 | use_toggle, 21 | updateValue, 22 | } = props; 23 | 24 | const toggleValue = () => updateValue(!value); 25 | 26 | return ( 27 | 28 | {use_toggle ? ( 29 | 34 | ) : ( 35 | 40 | )} 41 | 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /src/components/Checkbox/CheckboxMultiple.styl: -------------------------------------------------------------------------------- 1 | /** 2 | * CheckboxMultiple 3 | */ 4 | 5 | .pmc-setting-checkbox_multiple 6 | 7 | .components-base-control 8 | 9 | + .components-base-control 10 | margin-top 1em 11 | 12 | .components-checkbox-control__label 13 | font-weight 500 14 | -------------------------------------------------------------------------------- /src/components/Checkbox/CheckboxMultiple.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Component } from "@wordpress/element"; 3 | import { 4 | BaseControl, 5 | CheckboxControl, 6 | ToggleControl, 7 | } from "@wordpress/components"; 8 | import { withState } from "@wordpress/compose"; 9 | import { compact } from "lodash"; 10 | 11 | import "./CheckboxMultiple.styl"; 12 | 13 | interface WithStateProps { 14 | setState: SetState<{ 15 | options_prepared: { 16 | value: string; 17 | label: string; 18 | }[]; 19 | }>; 20 | options_prepared: Option[]; 21 | } 22 | 23 | interface OwnProps extends CheckboxMultipleProps, SettingPropsShared { 24 | updateValue: (value: string[]) => void; 25 | value: CheckboxMultipleProps["default_value"]; 26 | } 27 | 28 | interface Props extends WithStateProps, OwnProps {} 29 | 30 | interface Option { 31 | value: string; 32 | label: string; 33 | } 34 | 35 | export const CheckboxMultiple = withState({ options_prepared: [] })( 36 | class extends Component { 37 | prepareOptions() { 38 | const { value, options, default_value, setState } = this.props; 39 | const options_key = options.map(({ value }) => value); 40 | 41 | // If there were old selected values that are no longer included 42 | // in the options array, we still display them. 43 | // But when they are deselected and the post is saved they will no longer appear. 44 | const old_options = compact(value).reduce( 45 | (old_options, value) => { 46 | if (options_key.includes(value)) { 47 | return old_options; 48 | } 49 | 50 | return old_options.concat({ 51 | value: value, 52 | label: value, 53 | }); 54 | }, 55 | [] 56 | ); 57 | 58 | const old_options_key = old_options.map(({ value }) => value); 59 | 60 | // If there are values in the default options which are not included 61 | // in the options array, we display them. 62 | const default_options = default_value.reduce( 63 | (default_options, value) => { 64 | if ( 65 | options_key.includes(value) || 66 | old_options_key.includes(value) 67 | ) { 68 | return default_options; 69 | } 70 | 71 | return default_options.concat({ value, label: value }); 72 | }, 73 | [] 74 | ); 75 | 76 | setState({ 77 | options_prepared: [ 78 | ...options, 79 | ...old_options, 80 | ...default_options, 81 | ], 82 | }); 83 | } 84 | 85 | componentDidMount() { 86 | this.prepareOptions(); 87 | } 88 | 89 | componentDidUpdate(prev_props: OwnProps) { 90 | if (this.props.options.length > prev_props.options.length) { 91 | this.prepareOptions(); 92 | } 93 | } 94 | 95 | render() { 96 | const { 97 | id, 98 | label, 99 | help, 100 | value, 101 | use_toggle, 102 | updateValue, 103 | options_prepared, 104 | } = this.props; 105 | 106 | const onChange = (selected: boolean, option_value: string) => { 107 | let value_updated; 108 | 109 | if (selected) { 110 | value_updated = value.includes(option_value) 111 | ? value 112 | : value.concat(option_value); 113 | } else { 114 | value_updated = value.filter( 115 | value => value !== option_value 116 | ); 117 | } 118 | 119 | // If there is no value selected we save an empty string. 120 | // This is needed because meta_key_exists would turn false otherwise, 121 | // which makes it impossible to differentiate between a post that has 122 | // no values selected and one which hasnt saved any value. 123 | // This permits us to use the default_value correctly. 124 | value_updated = value_updated.length ? value_updated : [""]; 125 | 126 | updateValue(value_updated); 127 | }; 128 | 129 | return ( 130 | 131 | {use_toggle 132 | ? options_prepared.map( 133 | ({ label, value: option_value }, index) => ( 134 | 139 | onChange(selected, option_value) 140 | } 141 | /> 142 | ) 143 | ) 144 | : options_prepared.map( 145 | ({ label, value: option_value }, index) => ( 146 | 151 | onChange(selected, option_value) 152 | } 153 | /> 154 | ) 155 | )} 156 | 157 | ); 158 | } 159 | } 160 | ); 161 | -------------------------------------------------------------------------------- /src/components/Checkbox/index.ts: -------------------------------------------------------------------------------- 1 | export { Checkbox } from "./Checkbox"; 2 | export { CheckboxMultiple } from "./CheckboxMultiple"; 3 | -------------------------------------------------------------------------------- /src/components/Color/Color.styl: -------------------------------------------------------------------------------- 1 | /** 2 | * Color 3 | */ 4 | 5 | .pmc-setting-color 6 | 7 | .components-color-palette + .components-range-control 8 | margin-top 1em 9 | 10 | .components-base-control > .components-base-control__field 11 | flex-wrap wrap 12 | 13 | .components-base-control__label 14 | display flex 15 | align-items center 16 | -------------------------------------------------------------------------------- /src/components/Color/Color.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import tinycolor from "tinycolor2"; 3 | import { isUndefined } from "lodash"; 4 | import { __ } from "@wordpress/i18n"; 5 | import { Component, Fragment } from "@wordpress/element"; 6 | import { 7 | ColorPalette, 8 | BaseControl, 9 | ColorIndicator, 10 | RangeControl, 11 | } from "@wordpress/components"; 12 | import { withState } from "@wordpress/compose"; 13 | 14 | import "./Color.styl"; 15 | import { Span } from "@/utils/components"; 16 | 17 | interface WithStateProps { 18 | setState: SetState<{ alpha: number; color: string }>; 19 | color: string; 20 | alpha: number; 21 | } 22 | 23 | interface OwnProps extends ColorProps, SettingPropsShared { 24 | updateValue: (value: string) => void; 25 | value: ColorProps["default_value"]; 26 | } 27 | 28 | interface Props extends WithStateProps, OwnProps {} 29 | 30 | export const Color = withState({ color: "", alpha: 100 })( 31 | class extends Component { 32 | componentDidMount() { 33 | const { setState, value, alpha_control } = this.props; 34 | 35 | const color_tiny = tinycolor(value); 36 | 37 | if (!color_tiny.isValid()) { 38 | return; 39 | } 40 | 41 | const alpha = alpha_control ? color_tiny.getAlpha() * 100 : 100; 42 | const color = color_tiny.toHexString(); 43 | 44 | setState({ alpha, color }); 45 | } 46 | 47 | render() { 48 | const { 49 | updateValue, 50 | id, 51 | color, 52 | alpha, 53 | palette, 54 | alpha_control, 55 | label, 56 | help, 57 | value, 58 | setState, 59 | } = this.props; 60 | 61 | return ( 62 | 66 | {label} 67 | 68 | 69 | } 70 | help={help} 71 | > 72 | { 78 | color = color || ""; 79 | 80 | setState({ color }); 81 | 82 | if (alpha_control) { 83 | const color_tiny = tinycolor(color); 84 | 85 | updateValue( 86 | color_tiny.isValid() 87 | ? color_tiny 88 | .setAlpha(alpha / 100) 89 | .toRgbString() 90 | : "" 91 | ); 92 | } else { 93 | updateValue(color); 94 | } 95 | }} 96 | /> 97 | 98 | {alpha_control && ( 99 | { 106 | alpha = isUndefined(alpha) ? 100 : alpha; 107 | 108 | setState({ alpha }); 109 | 110 | const color_tiny = tinycolor(color); 111 | 112 | updateValue( 113 | color_tiny.isValid() 114 | ? color_tiny 115 | .setAlpha(alpha / 100) 116 | .toRgbString() 117 | : "" 118 | ); 119 | }} 120 | /> 121 | )} 122 | 123 | ); 124 | } 125 | } 126 | ); 127 | -------------------------------------------------------------------------------- /src/components/Color/index.ts: -------------------------------------------------------------------------------- 1 | export { Color } from "./Color"; 2 | -------------------------------------------------------------------------------- /src/components/CustomText/CustomText.styl: -------------------------------------------------------------------------------- 1 | /** 2 | * CustomText 3 | */ 4 | 5 | .pmc-setting-custom_text 6 | 7 | ol 8 | list-style-type decimal 9 | margin 0 0 1em 2em 10 | 11 | ul 12 | list-style-type disc 13 | margin 0 0 1em 2em 14 | 15 | a 16 | display block 17 | 18 | a 19 | p 20 | margin-top 0 21 | margin-bottom 1em 22 | 23 | h3 24 | font-weight 600 25 | margin-top 0 26 | margin-bottom 1.5em 27 | 28 | ol 29 | ul 30 | a 31 | p 32 | h3 33 | &:last-child 34 | margin-bottom 0 35 | -------------------------------------------------------------------------------- /src/components/CustomText/CustomText.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Fragment } from "@wordpress/element"; 3 | 4 | import "./CustomText.styl"; 5 | import { A, P, H3, Ol, Ul, Li } from "@/utils/components"; 6 | 7 | interface Props extends CustomTextProps, SettingPropsShared {} 8 | 9 | export const CustomText: React.ComponentType = props => { 10 | const { content } = props; 11 | 12 | return ( 13 | 14 | {content.map((element, index) => ( 15 | 16 | {(() => { 17 | switch (element.type) { 18 | case "paragraph": 19 | case "p": 20 | return

{element.content}

; 21 | 22 | case "title": 23 | case "h3": 24 | return

{element.content}

; 25 | 26 | case "link": 27 | case "a": 28 | return ( 29 | 30 | {element.content} 31 | 32 | ); 33 | 34 | case "ordered_list": 35 | case "ol": 36 | return ( 37 |
    38 | {element.content.map((li, index) => ( 39 |
  1. {li}
  2. 40 | ))} 41 |
42 | ); 43 | 44 | case "unordered_list": 45 | case "ul": 46 | return ( 47 |
    48 | {element.content.map((li, index) => ( 49 |
  • {li}
  • 50 | ))} 51 |
52 | ); 53 | 54 | default: 55 | return null; 56 | } 57 | })()} 58 |
59 | ))} 60 |
61 | ); 62 | }; 63 | -------------------------------------------------------------------------------- /src/components/CustomText/index.ts: -------------------------------------------------------------------------------- 1 | export { CustomText } from "./CustomText"; 2 | -------------------------------------------------------------------------------- /src/components/Date/Date.styl: -------------------------------------------------------------------------------- 1 | /** 2 | * Date 3 | */ 4 | 5 | .pmc-setting-date_range 6 | .pmc-setting-date_single 7 | 8 | div 9 | max-width 100% 10 | 11 | .pmc-date-clear 12 | margin-top 15px 13 | 14 | .DayPickerNavigation_button__default 15 | background-color transparent 16 | 17 | .CalendarMonth_caption 18 | font-size 16px 19 | 20 | .DayPickerNavigation_button__horizontalDefault 21 | border-color transparent 22 | 23 | .DateRangePicker_picker 24 | .SingleDatePicker_picker 25 | max-width calc(100% + 2.4em) 26 | 27 | .DayPicker_weekHeader 28 | width calc(100% + 10px) 29 | margin-left -10px 30 | max-width none 31 | margin-top -8px 32 | 33 | .DayPicker_weekHeader_li 34 | max-width 14.28% 35 | 36 | .DateRangePickerInput 37 | .SingleDatePickerInput 38 | display flex 39 | flex-direction column 40 | 41 | .DateInput ~ .DateInput 42 | padding-top 1.2em 43 | 44 | .DateRangePicker_picker 45 | .SingleDatePicker_picker 46 | top 0!important 47 | margin-left -1.2em 48 | width calc(100% + 2.4em) 49 | 50 | .DayPicker__withBorder 51 | box-shadow none 52 | 53 | .DateRangePickerInput_arrow 54 | .SingleDateInput_arrow 55 | .DateInput_fang 56 | display none 57 | 58 | .CalendarMonthGrid 59 | .DateRangePicker_picker 60 | .SingleDatePicker_picker 61 | left 0 62 | 63 | .DateRangePicker_picker 64 | .SingleDatePicker_picker 65 | position relative 66 | 67 | .DateRangePicker 68 | .SingleDatePicker 69 | .DateRangePickerInput 70 | .SingleDatePickerInput 71 | width 100% 72 | 73 | .DateInput_input 74 | width 150px 75 | 76 | .CalendarDay__selected_span 77 | .CalendarDay__default 78 | .CalendarDay__blocked_out_of_range, .CalendarDay__blocked_out_of_range:active, .CalendarDay__blocked_out_of_range:hover 79 | border none 80 | -------------------------------------------------------------------------------- /src/components/Date/DateRange.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import moment from "moment"; 3 | import "react-dates/initialize"; 4 | import { DateRangePicker } from "react-dates"; 5 | import { __ } from "@wordpress/i18n"; 6 | import { withState } from "@wordpress/compose"; 7 | import { Component } from "@wordpress/element"; 8 | import { BaseControl, Button } from "@wordpress/components"; 9 | import { doAction } from "@wordpress/hooks"; 10 | 11 | import "./Date.styl"; 12 | import { addPrefix } from "@/utils/tools"; 13 | 14 | interface WithStateProps { 15 | setState: SetState<{ 16 | start_date: moment.Moment | null; 17 | end_date: moment.Moment | null; 18 | focused_input: "startDate" | "endDate" | null; 19 | }>; 20 | start_date: moment.Moment | null; 21 | end_date: moment.Moment | null; 22 | focused_input: "startDate" | "endDate" | null; 23 | } 24 | 25 | interface OwnProps extends DateRangeProps, SettingPropsShared { 26 | updateValue: (value: [string, string] | [string]) => void; 27 | value: DateRangeProps["default_value"]; 28 | } 29 | 30 | interface Props extends OwnProps, WithStateProps {} 31 | 32 | // Trigger action that will register the locales, if one is set. 33 | // The corresponding addAction is inside the post-meta-controls-moment-locales script. 34 | doAction("postMetaControls.addMomentLocale", moment); 35 | 36 | export const DateRange: React.ComponentType = withState({ 37 | start_date: null, 38 | end_date: null, 39 | focused_input: null, 40 | })( 41 | class extends Component { 42 | constructor(props: Props) { 43 | super(props); 44 | 45 | // Specify the locale. 46 | moment.locale(props.locale); 47 | } 48 | 49 | componentDidMount() { 50 | const { value, setState, format } = this.props; 51 | 52 | if (value.length !== 2) { 53 | return; 54 | } 55 | 56 | const [start_date_raw, end_date_raw] = value; 57 | 58 | let start_date: moment.Moment | null = moment( 59 | start_date_raw, 60 | format 61 | ); 62 | let end_date: moment.Moment | null = moment(end_date_raw, format); 63 | 64 | if (!start_date.isValid()) { 65 | start_date = null; 66 | } 67 | 68 | if (!end_date.isValid()) { 69 | end_date = null; 70 | } 71 | 72 | setState({ start_date, end_date }); 73 | } 74 | 75 | render() { 76 | const { 77 | id, 78 | focused_input, 79 | minimum_days, 80 | maximum_days, 81 | start_date, 82 | end_date, 83 | format, 84 | setState, 85 | updateValue, 86 | label, 87 | help, 88 | unavailable_dates, 89 | } = this.props; 90 | 91 | return ( 92 | 93 | { 108 | setState({ start_date, end_date }); 109 | 110 | if (start_date && end_date) { 111 | updateValue([ 112 | start_date.format(format), 113 | end_date.format(format), 114 | ]); 115 | } 116 | }} 117 | focusedInput={focused_input} 118 | onFocusChange={focused_input => 119 | setState({ focused_input }) 120 | } 121 | isOutsideRange={day => { 122 | if ( 123 | focused_input === "endDate" && 124 | maximum_days && 125 | start_date 126 | ) { 127 | const is_outside = day.isAfter( 128 | start_date.clone().add(maximum_days, "days") 129 | ); 130 | 131 | if (is_outside) { 132 | return true; 133 | } 134 | } 135 | 136 | if (unavailable_dates.length) { 137 | const is_outside = unavailable_dates.reduce( 138 | (acc, day_raw) => { 139 | if (acc) { 140 | return true; 141 | } 142 | 143 | const [start_raw, end_raw] = day_raw; 144 | 145 | const start = 146 | start_raw === "today" 147 | ? moment() 148 | : moment(start_raw, format); 149 | 150 | const end = 151 | end_raw === "today" 152 | ? moment() 153 | : moment(end_raw, format); 154 | 155 | if (start_raw === "before") { 156 | return day.isBefore(end); 157 | } 158 | 159 | if (end_raw === "after") { 160 | return day.isSameOrAfter(start); 161 | } 162 | 163 | return day.isBetween( 164 | start, 165 | end.add(1, "day") 166 | ); 167 | }, 168 | false 169 | ); 170 | 171 | if (is_outside) { 172 | return true; 173 | } 174 | } 175 | 176 | return false; 177 | }} 178 | /> 179 | 200 | 201 | ); 202 | } 203 | } 204 | ); 205 | -------------------------------------------------------------------------------- /src/components/Date/DateSingle.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import moment, { Moment } from "moment"; 3 | import "react-dates/initialize"; 4 | import { SingleDatePicker } from "react-dates"; 5 | import { __ } from "@wordpress/i18n"; 6 | import { withState } from "@wordpress/compose"; 7 | import { Component } from "@wordpress/element"; 8 | import { BaseControl, Button } from "@wordpress/components"; 9 | import { doAction } from "@wordpress/hooks"; 10 | 11 | import { addPrefix } from "@/utils/tools"; 12 | 13 | interface WithStateProps { 14 | setState: SetState<{ 15 | date: Moment | null; 16 | focused: boolean; 17 | }>; 18 | date: Moment | null; 19 | focused: boolean; 20 | } 21 | 22 | interface OwnProps extends DateSingleProps, SettingPropsShared { 23 | updateValue: (value: string) => void; 24 | value: DateSingleProps["default_value"]; 25 | } 26 | 27 | interface Props extends OwnProps, WithStateProps {} 28 | 29 | // Trigger action that will register the locales, if one is set. 30 | // The corresponding addAction is inside the post-meta-controls-moment-locales script. 31 | doAction("postMetaControls.addMomentLocale", moment); 32 | 33 | export const DateSingle = withState({ 34 | date: null, 35 | focused: false, 36 | })( 37 | class extends Component { 38 | constructor(props: Props) { 39 | super(props); 40 | 41 | // Specify the locale. 42 | moment.locale(props.locale); 43 | } 44 | 45 | componentDidMount() { 46 | const { value, setState, format } = this.props; 47 | 48 | const date = moment(value, format); 49 | 50 | setState({ date: date.isValid() ? date : null }); 51 | } 52 | 53 | render() { 54 | const { 55 | id, 56 | focused, 57 | date, 58 | format, 59 | setState, 60 | updateValue, 61 | label, 62 | help, 63 | unavailable_dates, 64 | } = this.props; 65 | 66 | return ( 67 | 68 | { 76 | setState({ date }); 77 | 78 | if (date) { 79 | updateValue(date.format(format)); 80 | } 81 | }} 82 | focused={focused} 83 | onFocusChange={({ focused }) => setState({ focused })} 84 | id={`${id}-date_single`} 85 | isOutsideRange={day => { 86 | if (!unavailable_dates.length) { 87 | return false; 88 | } 89 | 90 | const is_outside_range = unavailable_dates.reduce( 91 | (acc, day_raw) => { 92 | if (acc) { 93 | return true; 94 | } 95 | 96 | const [start_raw, end_raw] = day_raw; 97 | 98 | const start = 99 | start_raw === "today" 100 | ? moment() 101 | : moment(start_raw, format); 102 | 103 | const end = 104 | end_raw === "today" 105 | ? moment() 106 | : moment(end_raw, format); 107 | 108 | if (start_raw === "before") { 109 | return day.isBefore(end); 110 | } 111 | 112 | if (end_raw === "after") { 113 | return day.isSameOrAfter(start); 114 | } 115 | 116 | return day.isBetween( 117 | start, 118 | end.add(1, "day") 119 | ); 120 | }, 121 | false 122 | ); 123 | 124 | return is_outside_range; 125 | }} 126 | /> 127 | 149 | 150 | ); 151 | } 152 | } 153 | ); 154 | -------------------------------------------------------------------------------- /src/components/Date/index.ts: -------------------------------------------------------------------------------- 1 | export { DateRange } from "./DateRange"; 2 | export { DateSingle } from "./DateSingle"; 3 | -------------------------------------------------------------------------------- /src/components/Image/Image.styl: -------------------------------------------------------------------------------- 1 | /** 2 | * Image 3 | */ 4 | 5 | +prefix-classes('pmc-') 6 | 7 | img.image 8 | object-fit contain 9 | width auto 10 | max-width 100% 11 | height auto 12 | max-height 100% 13 | 14 | .image-not_found 15 | background var(--border_color) 16 | padding 50px 17 | box-shadow 0 0 2px var(--border_color_softer) 18 | 19 | .images-container 20 | width calc(100% + 2.4em) 21 | margin-left -1.2em 22 | margin-top 2.4em 23 | 24 | .image-container 25 | border 1px dotted var(--border_color) 26 | margin -1px 27 | background-color var(--background_color) 28 | padding 30px 50px 29 | width 100% 30 | height 200px 31 | position relative 32 | display flex 33 | align-items center 34 | justify-content center 35 | 36 | &:hover 37 | button.image-remove 38 | fill var(--text_color) 39 | background-color var(--background_color_softer) 40 | 41 | .dragging_image 42 | .setting-image_multiple .image 43 | // https://stackoverflow.com/a/18294634 | CC BY-SA 3.0 44 | cursor move // fallback if grab cursor is unsupported 45 | cursor grab 46 | cursor -moz-grab 47 | cursor -webkit-grab 48 | 49 | button.image-remove 50 | padding 5px 51 | width 30px 52 | height 30px 53 | position absolute 54 | top 0 55 | right 0 56 | z-index 1 57 | transition fill .3s .1s $e_standard, background-color .3s .1s $e_standard 58 | background-color transparent 59 | margin 2px 60 | fill var(--text_color_softer) 61 | border-radius 50% 62 | 63 | svg 64 | height 20px 65 | 66 | .dragging_image 67 | z-index 999 68 | 69 | img 70 | max-width 100% 71 | 72 | .pmc-sidebar 73 | 74 | .pmc-image-button 75 | display block 76 | height 36px 77 | line-height 33px 78 | padding 0 10px 79 | 80 | .pmc-image-button 81 | .pmc-image-button:focus:enabled 82 | .components-color-palette__clear 83 | .components-color-palette__clear:focus:enabled 84 | .pmc-date-clear 85 | .pmc-date-clear:focus:enabled 86 | background var(--border_color_softer) 87 | color var(--text_color_softer) 88 | border 1px solid var(--border_color) 89 | box-shadow inset 0 -1px 0 var(--border_color) 90 | border-radius 3px 91 | transition background-color 0.15s cubic-bezier(0, 0, 0.2, 1) 92 | 93 | &:hover 94 | background var(--border_color) 95 | color var(--text_color_softer) 96 | -------------------------------------------------------------------------------- /src/components/Image/Image.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { castArray } from "lodash"; 3 | import { __ } from "@wordpress/i18n"; 4 | import { MediaUpload } from "@wordpress/block-editor"; 5 | import { Component } from "@wordpress/element"; 6 | import { Button, BaseControl } from "@wordpress/components"; 7 | import { withState } from "@wordpress/compose"; 8 | import apiFetch from "@wordpress/api-fetch"; 9 | 10 | import "./Image.styl"; 11 | import { 12 | addPrefix, 13 | prepareImageDataFromMedia, 14 | prepareImageDataFromRest, 15 | } from "@/utils/tools"; 16 | import { Div, Img, Icon } from "@/utils/components"; 17 | 18 | interface WithStateProps { 19 | setState: SetState<{ 20 | url: string; 21 | alt: string; 22 | image_id_not_found: number; 23 | }>; 24 | url: string; 25 | alt: string; 26 | image_id_not_found: number; 27 | } 28 | 29 | interface OwnProps extends ImageProps, SettingPropsShared { 30 | updateValue: (value: number) => void; 31 | value: ImageProps["default_value"]; 32 | } 33 | 34 | interface Props extends OwnProps, WithStateProps {} 35 | 36 | export const Image: React.ComponentType = withState({ 37 | url: "", 38 | alt: "", 39 | image_id_not_found: 0, 40 | })( 41 | class extends Component { 42 | componentDidMount() { 43 | const { value: image_id, setState } = this.props; 44 | 45 | if (image_id === 0) { 46 | return; 47 | } 48 | 49 | apiFetch({ 50 | path: `wp/v2/media/${image_id}`, 51 | }) 52 | .then(data_raw => { 53 | const data = prepareImageDataFromRest(castArray(data_raw)); 54 | 55 | // The sorting of the elements from data_raw is not 56 | // the same as the one from the so we need to order it. 57 | const { url, alt } = data[0]; 58 | 59 | setState({ url, alt }); 60 | }) 61 | .catch(() => setState({ image_id_not_found: image_id })); 62 | } 63 | 64 | render() { 65 | const { 66 | id, 67 | updateValue, 68 | setState, 69 | value: image_id, 70 | label, 71 | help, 72 | url, 73 | alt, 74 | image_id_not_found, 75 | } = this.props; 76 | 77 | const updateImage = (data_raw: ImageFromMedia) => { 78 | const data = prepareImageDataFromMedia(castArray(data_raw)); 79 | const { id, url, alt } = data[0]; 80 | 81 | updateValue(id); 82 | setState({ url, alt, image_id_not_found: 0 }); 83 | }; 84 | 85 | const removeImage = () => { 86 | updateValue(0); 87 | setState({ url: "", alt: "", image_id_not_found: 0 }); 88 | }; 89 | 90 | return ( 91 | 92 | ( 97 | 103 | )} 104 | /> 105 |
106 | {!!image_id_not_found && ( 107 |
108 |
{`Image with id ${image_id_not_found} was not found.`}
109 | 115 |
116 | )} 117 | 118 | {!image_id_not_found && url && ( 119 |
120 | {alt} 121 | 127 |
128 | )} 129 |
130 |
131 | ); 132 | } 133 | } 134 | ); 135 | -------------------------------------------------------------------------------- /src/components/Image/index.ts: -------------------------------------------------------------------------------- 1 | export { Image } from "./Image"; 2 | export { ImageMultiple } from "./ImageMultiple"; 3 | -------------------------------------------------------------------------------- /src/components/Panel/PanelCollapsible.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Fragment } from "@wordpress/element"; 3 | import { PanelBody } from "@wordpress/components"; 4 | 5 | import { Div, Span } from "@/utils/components"; 6 | import { addPrefix } from "@/utils/tools"; 7 | import { Settings } from "../Setting"; 8 | import { PanelPrepared } from "./Panels"; 9 | 10 | export const PanelCollapsible: React.ComponentType = props => { 11 | const { label, initial_open, icon, id } = props; 12 | 13 | return ( 14 | 18 | {icon} 19 | {label} 20 | 21 | } 22 | id={id} 23 | initialOpen={initial_open} 24 | className={addPrefix(["panel", "panel-collapsible"])} 25 | > 26 |
27 | 28 |
29 |
30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /src/components/Panel/PanelNotCollapsible.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { PanelRow } from "@wordpress/components"; 3 | 4 | import { Span, Div } from "@/utils/components"; 5 | import { Settings } from "../Setting"; 6 | import { PanelPrepared } from "./Panels"; 7 | 8 | export const PanelNotCollapsible: React.ComponentType = props => { 9 | const { label, icon, id } = props; 10 | 11 | return ( 12 |
13 | {label && ( 14 |
15 | {icon} 16 | {label} 17 |
18 | )} 19 | 20 | 21 | 22 |
23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /src/components/Panel/Panels.styl: -------------------------------------------------------------------------------- 1 | /** 2 | * Panel 3 | */ 4 | 5 | +prefix-classes('pmc-') 6 | 7 | .panel + .panel 8 | border-top 1.5px solid var(--border_color) 9 | 10 | .panel-label-container 11 | font-weight 600 12 | padding 15px 13 | position relative 14 | text-align left 15 | width 100% 16 | display flex 17 | 18 | .panel-icon svg 19 | .panel-icon span 20 | max-height 18px 21 | display block 22 | margin-right 8px 23 | 24 | .panel-content 25 | display flex 26 | flex-direction column 27 | 28 | .pmc-tab 29 | 30 | .components-panel__body 31 | border-bottom none 32 | 33 | &:first-child 34 | border-top none 35 | 36 | .components-panel__row 37 | margin 0 38 | padding 0 39 | display flex 40 | flex-direction column 41 | 42 | .pmc-panel 43 | padding 0 44 | 45 | > h2.components-panel__body-title 46 | margin 0 47 | 48 | button:focus 49 | background transparent 50 | 51 | &:last-child 52 | .components-panel__row 53 | padding-bottom 100px 54 | -------------------------------------------------------------------------------- /src/components/Panel/Panels.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { withSelect } from "@wordpress/data"; 3 | import { Component, Fragment } from "@wordpress/element"; 4 | import { compose, withState } from "@wordpress/compose"; 5 | 6 | import "./Panels.styl"; 7 | import { PanelCollapsible } from "./PanelCollapsible"; 8 | import { PanelNotCollapsible } from "./PanelNotCollapsible"; 9 | import { store_slug } from "@/utils/data"; 10 | import { prepareIcon } from "@/utils/tools"; 11 | 12 | interface OwnProps { 13 | tab_id: TabProps["id"]; 14 | } 15 | 16 | interface WithSelectProps { 17 | panels: PanelProps[]; 18 | } 19 | 20 | interface WithStateProps { 21 | setState: SetState<{ panels_prepared: PanelPrepared[] }>; 22 | panels_prepared: PanelPrepared[]; 23 | } 24 | 25 | export interface PanelPrepared 26 | extends Omit { 27 | icon: React.ReactNode; 28 | } 29 | 30 | interface Props extends WithSelectProps, WithStateProps, OwnProps {} 31 | 32 | export const Panels: React.ComponentType = compose([ 33 | withState({ panels_prepared: [] }), 34 | withSelect((select, { tab_id }) => ({ 35 | panels: select(store_slug).getPanels(tab_id), 36 | })), 37 | ])( 38 | class extends Component { 39 | preparePanels() { 40 | const { setState, panels } = this.props; 41 | 42 | setState({ 43 | panels_prepared: panels.map(panel => { 44 | const { icon_svg, icon_dashicon, ...rest } = panel; 45 | 46 | return { 47 | ...rest, 48 | icon: prepareIcon(icon_svg, icon_dashicon, "panel"), 49 | }; 50 | }), 51 | }); 52 | } 53 | 54 | componentDidMount() { 55 | this.preparePanels(); 56 | } 57 | 58 | componentDidUpdate(prev_props: Props) { 59 | const { tab_id, panels } = this.props; 60 | 61 | if ( 62 | tab_id !== prev_props.tab_id || 63 | panels.length > prev_props.panels.length 64 | ) { 65 | this.preparePanels(); 66 | } 67 | } 68 | 69 | render() { 70 | const { panels_prepared } = this.props; 71 | 72 | return ( 73 | 74 | {panels_prepared.map(panel => 75 | panel.collapsible ? ( 76 | 77 | ) : ( 78 | 79 | ) 80 | )} 81 | 82 | ); 83 | } 84 | } 85 | ); 86 | -------------------------------------------------------------------------------- /src/components/Panel/index.ts: -------------------------------------------------------------------------------- 1 | export { Panels } from "./Panels"; 2 | export { PanelCollapsible } from "./PanelCollapsible"; 3 | export { PanelNotCollapsible } from "./PanelNotCollapsible"; 4 | -------------------------------------------------------------------------------- /src/components/Radio/Radio.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { RadioControl } from "@wordpress/components"; 3 | 4 | interface Props extends RadioProps, SettingPropsShared { 5 | updateValue: (value: string) => void; 6 | value: RadioProps["default_value"]; 7 | } 8 | 9 | export const Radio: React.ComponentType = props => { 10 | const { options, label, help, value, updateValue } = props; 11 | 12 | return ( 13 | 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /src/components/Radio/index.ts: -------------------------------------------------------------------------------- 1 | export { Radio } from "./Radio"; 2 | -------------------------------------------------------------------------------- /src/components/Range/Range.styl: -------------------------------------------------------------------------------- 1 | /** 2 | * Range 3 | */ 4 | 5 | .pmc-setting .components-range-control 6 | .pmc-setting.components-range-control 7 | 8 | .components-base-control__slider 9 | flex 1 10 | margin-left 0 11 | 12 | .components-base-control__label 13 | min-width 100% 14 | margin-right 0 15 | 16 | .components-base-control__field 17 | display flex 18 | flex-wrap wrap 19 | 20 | .pmc-setting-range_float 21 | 22 | &.float_5 23 | input[type="number"] 24 | width 60px 25 | &.float_6 26 | input[type="number"] 27 | width 70px 28 | &.float_7 29 | input[type="number"] 30 | width 80px 31 | &.float_8 32 | input[type="number"] 33 | width 90px 34 | &.float_9 35 | input[type="number"] 36 | width 100px 37 | &.float_10 38 | input[type="number"] 39 | width 110px 40 | &.float_11 41 | input[type="number"] 42 | width 120px 43 | &.float_12 44 | input[type="number"] 45 | width 130px 46 | 47 | // https://css-tricks.com/styling-cross-browser-compatible-range-inputs-css/ 48 | .pmc-sidebar 49 | 50 | input[type=range] 51 | -webkit-appearance: none; 52 | 53 | input[type=range]:focus 54 | outline none 55 | box-shadow none 56 | 57 | input[type=range]::-webkit-slider-runnable-track 58 | background-color var(--border_color) 59 | border none 60 | box-shadow none 61 | 62 | input[type=range]::-webkit-slider-thumb 63 | box-shadow none 64 | border none 65 | height 12px 66 | width 12px 67 | border-radius 50% 68 | background-color var(--text_color_softer) 69 | cursor pointer 70 | -webkit-appearance none 71 | margin-top -5px 72 | 73 | input[type=range]:focus::-webkit-slider-runnable-track 74 | background-color var(--border_color) 75 | border none 76 | box-shadow none 77 | outline none 78 | 79 | input[type=range]::-moz-range-track 80 | background-color var(--border_color) 81 | border none 82 | box-shadow none 83 | 84 | input[type=range]::-moz-range-thumb 85 | box-shadow none 86 | border none 87 | height 12px 88 | width 12px 89 | border-radius 50% 90 | background-color var(--text_color_softer) 91 | cursor pointer 92 | -webkit-appearance none 93 | margin-top -5px 94 | 95 | input[type=range]::-ms-track 96 | background-color var(--border_color) 97 | border none 98 | box-shadow none 99 | 100 | input[type=range]::-ms-fill-lower 101 | background-color var(--border_color) 102 | border none 103 | box-shadow none 104 | 105 | input[type=range]::-ms-fill-upper 106 | background-color var(--border_color) 107 | border none 108 | box-shadow none 109 | 110 | input[type=range]::-ms-thumb 111 | box-shadow none 112 | border none 113 | height 12px 114 | width 12px 115 | border-radius 50% 116 | background-color var(--text_color_softer) 117 | cursor pointer 118 | -webkit-appearance none 119 | margin-top -5px 120 | 121 | input[type=range]:focus::-ms-fill-lower 122 | background-color var(--border_color) 123 | border none 124 | box-shadow none 125 | outline none 126 | 127 | input[type=range]:focus::-ms-fill-upper 128 | background-color var(--border_color) 129 | border none 130 | box-shadow none 131 | outline none 132 | -------------------------------------------------------------------------------- /src/components/Range/Range.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { RangeControl } from "@wordpress/components"; 3 | 4 | import "./Range.styl"; 5 | 6 | interface Props extends RangeProps, SettingPropsShared { 7 | updateValue: (value: number) => void; 8 | value: RangeProps["default_value"]; 9 | } 10 | 11 | export const Range: React.ComponentType = props => { 12 | const { min, max, step, label, help, value, updateValue } = props; 13 | 14 | return ( 15 | 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /src/components/Range/RangeFloat.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { RangeControl } from "@wordpress/components"; 3 | import { withState } from "@wordpress/compose"; 4 | import { Component } from "@wordpress/element"; 5 | 6 | interface WithStateProps { 7 | setState: SetState<{ digits_length: number }>; 8 | digits_length: string; 9 | } 10 | 11 | interface OwnProps extends RangeFloatProps, SettingPropsShared { 12 | updateValue: (value: number) => void; 13 | value: RangeFloatProps["default_value"]; 14 | } 15 | 16 | interface Props extends OwnProps, WithStateProps {} 17 | 18 | export const RangeFloat: React.ComponentType = withState({ 19 | digits_length: 1, 20 | })( 21 | class extends Component { 22 | componentDidMount() { 23 | const { max, setState } = this.props; 24 | 25 | setState({ digits_length: Math.round(max).toString().length + 2 }); 26 | } 27 | 28 | render() { 29 | const { 30 | min, 31 | max, 32 | step, 33 | label, 34 | help, 35 | digits_length, 36 | value, 37 | updateValue, 38 | } = this.props; 39 | 40 | return ( 41 | 51 | ); 52 | } 53 | } 54 | ); 55 | -------------------------------------------------------------------------------- /src/components/Range/index.ts: -------------------------------------------------------------------------------- 1 | export { Range } from "./Range"; 2 | export { RangeFloat } from "./RangeFloat"; 3 | -------------------------------------------------------------------------------- /src/components/Select/Select.styl: -------------------------------------------------------------------------------- 1 | /** 2 | * Select 3 | */ 4 | 5 | .pmc-control-select 6 | 7 | select 8 | overflow auto 9 | outline-color transparent 10 | box-shadow none 11 | -------------------------------------------------------------------------------- /src/components/Select/Select.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { SelectControl } from "@wordpress/components"; 3 | 4 | import "./Select.styl"; 5 | 6 | interface Props extends SelectProps, SettingPropsShared { 7 | updateValue: (value: string) => void; 8 | value: SelectProps["default_value"]; 9 | } 10 | 11 | export const Select: React.ComponentType = props => { 12 | const { options, label, help, value, updateValue } = props; 13 | 14 | return ( 15 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /src/components/Select/index.ts: -------------------------------------------------------------------------------- 1 | export { Select } from "./Select"; 2 | -------------------------------------------------------------------------------- /src/components/Setting/Setting.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { withMetaData } from "./withMetaData"; 3 | import { withLocalData } from "./withLocalData"; 4 | import { withNoneData } from "./withNoneData"; 5 | 6 | import { Div } from "@/utils/components"; 7 | import { Buttons } from "../Buttons"; 8 | import { Checkbox } from "../Checkbox"; 9 | import { CheckboxMultiple } from "../Checkbox"; 10 | import { Color } from "../Color"; 11 | import { CustomText } from "../CustomText"; 12 | import { DateRange } from "../Date"; 13 | import { DateSingle } from "../Date"; 14 | import { Image } from "../Image"; 15 | import { ImageMultiple } from "../Image"; 16 | import { Radio } from "../Radio"; 17 | import { Range } from "../Range"; 18 | import { RangeFloat } from "../Range"; 19 | import { Select } from "../Select"; 20 | import { Text } from "../Text"; 21 | import { Textarea } from "../Textarea"; 22 | 23 | type Props = SettingProps & { 24 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 25 | value: any; 26 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 27 | updateValue: (value: any) => void; 28 | }; 29 | 30 | type Controls = Record; 31 | 32 | const controls: Controls = { 33 | buttons: Buttons, 34 | checkbox: Checkbox, 35 | checkbox_multiple: CheckboxMultiple, 36 | color: Color, 37 | custom_text: CustomText, 38 | date_range: DateRange, 39 | date_single: DateSingle, 40 | image: Image, 41 | image_multiple: ImageMultiple, 42 | radio: Radio, 43 | range: Range, 44 | range_float: RangeFloat, 45 | select: Select, 46 | text: Text, 47 | textarea: Textarea, 48 | }; 49 | 50 | export const Setting: React.ComponentType = props => { 51 | const { type, ui_border_top, id } = props; 52 | const Control = controls[type]; 53 | 54 | return ( 55 |
63 | {Control && } 64 |
65 | ); 66 | }; 67 | 68 | export const SettingWithMetaData = withMetaData(Setting); 69 | 70 | export const SettingWithLocalData = withLocalData(Setting); 71 | 72 | export const SettingWithNoneData = withNoneData(Setting); 73 | -------------------------------------------------------------------------------- /src/components/Setting/Settings.styl: -------------------------------------------------------------------------------- 1 | /** 2 | * Settings 3 | */ 4 | 5 | +prefix-classes('pmc-') 6 | 7 | .setting 8 | position relative 9 | padding 2em 1.2em 10 | width 100% 11 | margin 0 12 | 13 | + .setting 14 | border-top 1px solid var(--border_color) 15 | + .warning 16 | border-top 4px solid transparent 17 | 18 | &:first-child 19 | border-top 1px dotted var(--border_color) 20 | 21 | &.no_border_top 22 | border-top none 23 | padding-top .5em 24 | 25 | .pmc-sidebar 26 | 27 | .components-base-control__help 28 | font-style normal 29 | margin-top 15px 30 | opacity .8 31 | 32 | .components-base-control__label 33 | font-weight 600 34 | margin-bottom 1.5em 35 | -------------------------------------------------------------------------------- /src/components/Setting/Settings.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { withSelect } from "@wordpress/data"; 3 | import { Fragment } from "@wordpress/element"; 4 | 5 | import "./Settings.styl"; 6 | import { store_slug } from "@/utils/data"; 7 | import { 8 | SettingWithMetaData, 9 | SettingWithLocalData, 10 | SettingWithNoneData, 11 | } from "./Setting"; 12 | 13 | interface WithSelectProps { 14 | settings: SettingProps[]; 15 | } 16 | 17 | interface OwnProps { 18 | panel_id: PanelProps["id"]; 19 | } 20 | 21 | export const Settings: React.ComponentType = withSelect< 22 | WithSelectProps, 23 | OwnProps 24 | >((select, { panel_id }) => ({ 25 | settings: select(store_slug).getSettings(panel_id), 26 | }))(props => { 27 | const { settings } = props; 28 | 29 | return ( 30 | 31 | {settings.map(setting => 32 | setting.data_type === "meta" ? ( 33 | 34 | ) : setting.data_type === "localstorage" ? ( 35 | 36 | ) : ( 37 | 38 | ) 39 | )} 40 | 41 | ); 42 | }); 43 | -------------------------------------------------------------------------------- /src/components/Setting/index.ts: -------------------------------------------------------------------------------- 1 | export { Setting } from "./Setting"; 2 | export { Settings } from "./Settings"; 3 | -------------------------------------------------------------------------------- /src/components/Setting/withLocalData.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { isUndefined } from "lodash"; 3 | import { withSelect, withDispatch } from "@wordpress/data"; 4 | import { compose } from "@wordpress/compose"; 5 | 6 | import { store_slug } from "@/utils/data"; 7 | 8 | interface WithSelectProps { 9 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 10 | value: any; 11 | } 12 | 13 | interface WithDispatchProps { 14 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 15 | updateValue: (value: any) => void; 16 | } 17 | 18 | interface OwnProps extends SettingPropsShared { 19 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 20 | default_value: any; 21 | data_type: "none" | "meta" | "localstorage"; 22 | } 23 | 24 | interface Props extends OwnProps, WithSelectProps, WithDispatchProps {} 25 | 26 | export const withLocalData = compose( 27 | withDispatch( 28 | (dispatch, { id: setting_id }) => ({ 29 | updateValue: value => 30 | dispatch(store_slug).updatePropLocal({ 31 | setting_id, 32 | value, 33 | }), 34 | }) 35 | ), 36 | 37 | withSelect( 38 | (select, { data_key_with_prefix, default_value }) => { 39 | const persisted: Obj = select(store_slug).getSettingsPersisted(); 40 | 41 | return { 42 | value: !isUndefined(persisted[data_key_with_prefix]) 43 | ? persisted[data_key_with_prefix] 44 | : default_value, 45 | }; 46 | } 47 | ), 48 | 49 | (WrappedComponent: React.ComponentType) => (props: Props) => { 50 | return ; 51 | } 52 | ); 53 | -------------------------------------------------------------------------------- /src/components/Setting/withMetaData.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { withSelect, withDispatch } from "@wordpress/data"; 3 | import { compose } from "@wordpress/compose"; 4 | import { Component } from "@wordpress/element"; 5 | import { isUndefined } from "lodash"; 6 | 7 | import { store_slug } from "@/utils/data"; 8 | 9 | interface WithSelectProps { 10 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 11 | value: any; 12 | } 13 | 14 | interface WithDispatchProps { 15 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 16 | updateValue: (value: any) => void; 17 | } 18 | 19 | interface OwnProps extends SettingPropsShared { 20 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 21 | default_value: any; 22 | } 23 | 24 | interface Props extends OwnProps, WithSelectProps, WithDispatchProps {} 25 | 26 | export const withMetaData = compose( 27 | withDispatch( 28 | (dispatch, { meta_key_exists, data_key_with_prefix }) => ({ 29 | updateValue: value => { 30 | // Save the value to the "core/editor" store. 31 | dispatch("core/editor").editPost({ 32 | meta: { [data_key_with_prefix]: value }, 33 | }); 34 | 35 | if (!meta_key_exists) { 36 | dispatch(store_slug).setMetaKeyExists(data_key_with_prefix); 37 | } 38 | }, 39 | }) 40 | ), 41 | 42 | withSelect( 43 | (select, { data_key_with_prefix, default_value, meta_key_exists }) => { 44 | const meta: Obj | undefined = select( 45 | "core/editor" 46 | ).getEditedPostAttribute("meta"); 47 | 48 | return { 49 | value: 50 | meta && meta_key_exists 51 | ? meta[data_key_with_prefix] 52 | : default_value, 53 | }; 54 | } 55 | ), 56 | 57 | (WrappedComponent: React.ComponentType) => 58 | class extends Component { 59 | // When publishing the post getEditedPostAttribute/getCurrentPostAttribute 60 | // return an object only with the keys that changed. The value 61 | // passed for any key which didnt change is undefined. We check it and 62 | // prevent the component from rendering. 63 | shouldComponentUpdate(next_props: Props) { 64 | if (isUndefined(next_props.value)) { 65 | return false; 66 | } 67 | 68 | return true; 69 | } 70 | 71 | render() { 72 | return ; 73 | } 74 | } 75 | ); 76 | -------------------------------------------------------------------------------- /src/components/Setting/withNoneData.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { isUndefined } from "lodash"; 3 | import { withSelect, withDispatch } from "@wordpress/data"; 4 | import { compose } from "@wordpress/compose"; 5 | 6 | import { store_slug } from "@/utils/data"; 7 | 8 | interface WithSelectProps { 9 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 10 | value: any; 11 | } 12 | 13 | interface WithDispatchProps { 14 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 15 | updateValue: (value: any) => void; 16 | } 17 | 18 | interface OwnProps extends SettingPropsShared { 19 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 20 | default_value: any; 21 | data_type: "none" | "meta" | "localstorage"; 22 | } 23 | 24 | interface Props extends OwnProps, WithSelectProps, WithDispatchProps {} 25 | 26 | export const withNoneData = compose( 27 | withDispatch( 28 | (dispatch, { id: setting_id }) => ({ 29 | updateValue: value => 30 | dispatch(store_slug).updatePropNone({ 31 | setting_id, 32 | value, 33 | }), 34 | }) 35 | ), 36 | 37 | withSelect((select, { id, default_value }) => { 38 | const data: Obj = select(store_slug).getSettingsNone(); 39 | 40 | return { 41 | value: !isUndefined(data[id]) ? data[id] : default_value, 42 | }; 43 | }), 44 | 45 | (WrappedComponent: React.ComponentType) => (props: Props) => { 46 | return ; 47 | } 48 | ); 49 | -------------------------------------------------------------------------------- /src/components/Sidebar/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Component } from "@wordpress/element"; 3 | import { withSelect } from "@wordpress/data"; 4 | import { compose, withState } from "@wordpress/compose"; 5 | 6 | import { store_slug } from "@/utils/data"; 7 | import { getColorScheme } from "@/utils/tools"; 8 | import { Div } from "@/utils/components"; 9 | import { Warnings } from "../Warnings"; 10 | import { Tabs } from "../Tab"; 11 | 12 | interface OwnProps { 13 | id: SidebarProps["id"]; 14 | } 15 | 16 | interface WithStateProps { 17 | setState: SetState<{ color_scheme: { id: string; type: string } }>; 18 | color_scheme: { id: string; type: string }; 19 | } 20 | 21 | interface WithSelectProps { 22 | sidebar: SidebarProps; 23 | warnings: SidebarProps["warnings"]; 24 | } 25 | 26 | interface Props extends OwnProps, WithStateProps, WithSelectProps {} 27 | 28 | export const Sidebar = compose([ 29 | withState({ color_scheme: { id: "", type: "" } }), 30 | withSelect((select, { id }) => ({ 31 | sidebar: select(store_slug).getSidebar(id), 32 | warnings: select(store_slug).getWarnings(id), 33 | })), 34 | ])( 35 | class extends Component { 36 | componentDidMount() { 37 | const { sidebar, setState } = this.props; 38 | 39 | setState({ color_scheme: getColorScheme(sidebar.ui_color_scheme) }); 40 | } 41 | 42 | render() { 43 | const { id, warnings, color_scheme } = this.props; 44 | 45 | return ( 46 |
56 | {warnings.length ? ( 57 | 58 | ) : ( 59 | 60 | )} 61 |
62 | ); 63 | } 64 | } 65 | ); 66 | -------------------------------------------------------------------------------- /src/components/Sidebar/index.ts: -------------------------------------------------------------------------------- 1 | export { Sidebar } from "./Sidebar"; 2 | -------------------------------------------------------------------------------- /src/components/Tab/Tab.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Div } from "@/utils/components"; 3 | import { Panels } from "../Panel"; 4 | 5 | interface Props { 6 | id: TabProps["id"]; 7 | } 8 | 9 | export const Tab: React.ComponentType = props => { 10 | const { id } = props; 11 | 12 | return ( 13 |
14 | 15 |
16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /src/components/Tab/Tabs.styl: -------------------------------------------------------------------------------- 1 | /** 2 | * Tab 3 | */ 4 | 5 | .pmc-tab-content 6 | position relative 7 | 8 | .pmc-sidebar 9 | 10 | .components-tab-panel__tabs 11 | display flex 12 | height 50px 13 | 14 | .pmc-tab-icon 15 | 16 | svg 17 | span 18 | max-height 18px 19 | display block 20 | margin-right 8px 21 | 22 | #editor button.pmc-tab-button 23 | 24 | & 25 | &:hover 26 | &:focus 27 | color inherit 28 | padding 5px 29 | height 100% 30 | box-shadow none 31 | width 50% 32 | background transparent 33 | transition background-color 0.15s cubic-bezier(0, 0, 0.2, 1) 34 | display flex 35 | align-items center 36 | justify-content center 37 | outline-color currentColor 38 | outline-width 1px 39 | cursor pointer 40 | border none 41 | transition background-color .15s $e_deceleration 42 | outline none 43 | border-bottom 1px solid var(--border_color) 44 | 45 | &:hover 46 | background-color var(--border_color_softer) 47 | 48 | &.is-active 49 | margin-bottom -1px 50 | font-weight 600 51 | padding-bottom 3px 52 | border-bottom 3px solid var(--color_primary) 53 | -------------------------------------------------------------------------------- /src/components/Tab/Tabs.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { compose, withState } from "@wordpress/compose"; 3 | import { withSelect, withDispatch } from "@wordpress/data"; 4 | import { Component, Fragment } from "@wordpress/element"; 5 | import { TabPanel } from "@wordpress/components"; 6 | 7 | import "./Tabs.styl"; 8 | import { Span } from "@/utils/components"; 9 | import { store_slug } from "@/utils/data"; 10 | import { addPrefix, prepareIcon } from "@/utils/tools"; 11 | import { Tab } from "./Tab"; 12 | 13 | export interface TabsPrepared { 14 | name: TabProps["id"]; 15 | title: React.ReactNode; 16 | className: string; 17 | } 18 | 19 | interface WithStateProps { 20 | setState: SetState<{ tabs_prepared: TabsPrepared[] }>; 21 | tabs_prepared: TabsPrepared[]; 22 | } 23 | 24 | interface OwnProps { 25 | sidebar_id: SidebarProps["id"]; 26 | } 27 | 28 | interface WithSelectProps { 29 | tabs: TabProps[]; 30 | active_tab: TabProps["id"]; 31 | } 32 | 33 | type WithDispatchProps = Pick; 34 | 35 | interface Props 36 | extends WithStateProps, 37 | WithSelectProps, 38 | OwnProps, 39 | WithDispatchProps {} 40 | 41 | export const Tabs = compose([ 42 | withState({ tabs_prepared: [] }), 43 | withDispatch(dispatch => ({ 44 | openTab: dispatch(store_slug).openTab, 45 | })), 46 | withSelect((select, { sidebar_id }) => ({ 47 | tabs: select(store_slug).getTabs(sidebar_id), 48 | active_tab: select(store_slug).getActiveTab(sidebar_id), 49 | })), 50 | ])( 51 | class Tabs extends Component { 52 | prepareTabs() { 53 | const { tabs, setState } = this.props; 54 | 55 | setState({ 56 | tabs_prepared: tabs.map( 57 | ({ label, icon_dashicon, icon_svg, id }) => ({ 58 | name: id, 59 | className: addPrefix("tab-button"), 60 | title: ( 61 | 62 | {prepareIcon(icon_svg, icon_dashicon, "tab")} 63 | {label} 64 | 65 | ), 66 | }) 67 | ), 68 | }); 69 | } 70 | 71 | componentDidMount() { 72 | this.prepareTabs(); 73 | } 74 | 75 | componentDidUpdate(prev_props: WithSelectProps) { 76 | if (this.props.tabs.length > prev_props.tabs.length) { 77 | this.prepareTabs(); 78 | } 79 | } 80 | 81 | render() { 82 | const { 83 | tabs, 84 | tabs_prepared, 85 | active_tab, 86 | openTab, 87 | sidebar_id, 88 | } = this.props; 89 | 90 | if (!tabs_prepared.length) { 91 | return null; 92 | } 93 | 94 | if (tabs.length === 1) { 95 | return ; 96 | } 97 | 98 | return ( 99 | 102 | openTab({ sidebar_id, tab_id: tab_name }) 103 | } 104 | // @ts-expect-error TODO: 105 | // Passing a title that includes an icon. 106 | // Confirm this type is accepted. 107 | tabs={tabs_prepared} 108 | initialTabName={active_tab} 109 | > 110 | {({ name }) => } 111 | 112 | ); 113 | } 114 | } 115 | ); 116 | -------------------------------------------------------------------------------- /src/components/Tab/index.ts: -------------------------------------------------------------------------------- 1 | export { Tab } from "./Tab"; 2 | export { Tabs } from "./Tabs"; 3 | -------------------------------------------------------------------------------- /src/components/Text/Text.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { TextControl } from "@wordpress/components"; 3 | 4 | interface Props extends TextProps, SettingPropsShared { 5 | updateValue: (value: string) => void; 6 | value: TextProps["default_value"]; 7 | } 8 | 9 | export const Text: React.ComponentType = props => { 10 | const { label, help, placeholder, value, updateValue } = props; 11 | 12 | return ( 13 | 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /src/components/Text/index.ts: -------------------------------------------------------------------------------- 1 | export { Text } from "./Text"; 2 | -------------------------------------------------------------------------------- /src/components/Textarea/Textarea.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { TextareaControl } from "@wordpress/components"; 3 | 4 | interface Props extends TextareaProps, SettingPropsShared { 5 | updateValue: (value: string) => void; 6 | value: TextareaProps["default_value"]; 7 | } 8 | 9 | export const Textarea: React.ComponentType = props => { 10 | const { label, help, placeholder, value, updateValue } = props; 11 | 12 | return ( 13 | 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /src/components/Textarea/index.ts: -------------------------------------------------------------------------------- 1 | export { Textarea } from "./Textarea"; 2 | -------------------------------------------------------------------------------- /src/components/Warnings/Warning.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { __, sprintf } from "@wordpress/i18n"; 3 | 4 | import { Div, Span } from "@/utils/components"; 5 | 6 | export const Warning: React.ComponentType = props => { 7 | const { title, message } = props; 8 | 9 | return ( 10 |
11 | {sprintf(__("%s:"), title)} 12 | {message} 13 |
14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /src/components/Warnings/Warnings.styl: -------------------------------------------------------------------------------- 1 | /** 2 | * Warnings 3 | */ 4 | 5 | +prefix-classes('pmc-') 6 | 7 | .warning-header 8 | position relative 9 | padding 2em 1.2em 10 | width 100% 11 | font-size 1.05em 12 | 13 | .warning 14 | color rgba(0,0,0,0.85) 15 | background-color #f9e2e2 16 | border-left 4px solid #d94f4f 17 | position relative 18 | padding 2em 1.2em 19 | width 100% 20 | 21 | .components-base-control__label 22 | position relative 23 | 24 | + .warning 25 | margin-top 5px 26 | 27 | .warning-title 28 | font-size 13px 29 | font-weight 500 30 | display block 31 | margin-bottom 5px 32 | -------------------------------------------------------------------------------- /src/components/Warnings/Warnings.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { __ } from "@wordpress/i18n"; 3 | import { Fragment } from "@wordpress/element"; 4 | 5 | import "./Warnings.styl"; 6 | import { Div, Span } from "@/utils/components"; 7 | import { Warning } from "./Warning"; 8 | 9 | interface Props { 10 | warnings: Warning[]; 11 | } 12 | 13 | export const Warnings: React.ComponentType = props => { 14 | const { warnings } = props; 15 | 16 | return ( 17 | 18 |
19 | {__("This sidebar has some invalid properties:")} 20 |
21 | 22 | {warnings.map(({ message, title }, index) => ( 23 | 24 | ))} 25 |
26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /src/components/Warnings/index.ts: -------------------------------------------------------------------------------- 1 | export { Warning } from "./Warning"; 2 | export { Warnings } from "./Warnings"; 3 | -------------------------------------------------------------------------------- /src/entry.ts: -------------------------------------------------------------------------------- 1 | import "./init/register-store"; 2 | import "./init/register-items"; 3 | -------------------------------------------------------------------------------- /src/init/register-store.ts: -------------------------------------------------------------------------------- 1 | import { registerStore } from "@wordpress/data"; 2 | 3 | import { store_slug } from "@/utils/data"; 4 | import { reducer } from "@/store/reducer"; 5 | import { actions } from "@/store/actions"; 6 | import { selectors } from "@/store/selectors"; 7 | 8 | registerStore(store_slug, { 9 | // @ts-expect-error TODO 10 | reducer, 11 | actions, 12 | selectors, 13 | persist: ["settings_persisted"], 14 | }); 15 | -------------------------------------------------------------------------------- /src/store/actions.ts: -------------------------------------------------------------------------------- 1 | export const actions: ActionCreators = { 2 | addPanel: payload => ({ 3 | type: "ADD_PANEL", 4 | payload, 5 | }), 6 | addSetting: payload => ({ 7 | type: "ADD_SETTING", 8 | payload, 9 | }), 10 | addSidebar: payload => ({ 11 | type: "ADD_SIDEBAR", 12 | payload, 13 | }), 14 | addTab: payload => ({ 15 | type: "ADD_TAB", 16 | payload, 17 | }), 18 | openTab: payload => ({ 19 | type: "OPEN_TAB", 20 | payload, 21 | }), 22 | setMetaKeyExists: payload => ({ 23 | type: "SET_META_KEY_EXISTS", 24 | payload, 25 | }), 26 | updatePropLocal: payload => ({ 27 | type: "UPDATE_PROP_LOCAL", 28 | payload, 29 | }), 30 | updatePropNone: payload => ({ 31 | type: "UPDATE_PROP_NONE", 32 | payload, 33 | }), 34 | }; 35 | -------------------------------------------------------------------------------- /src/store/reducer.ts: -------------------------------------------------------------------------------- 1 | import produce from "immer"; 2 | 3 | const initial_state: State = { 4 | panels: [], 5 | settings: [], 6 | settings_persisted: {}, 7 | settings_none: {}, 8 | sidebars: [], 9 | tabs: [], 10 | }; 11 | 12 | export const reducer = (state_prev = initial_state, action: Actions): State => { 13 | let state = state_prev; 14 | 15 | // Previous to WordPress 5.2, "persistence" takes the saved properties as the initial_state. 16 | // So we need to include the initial_state properties in the first reducer call. 17 | if (!state_prev.sidebars) { 18 | state = { ...initial_state, ...state }; 19 | } 20 | 21 | switch (action.type) { 22 | case "ADD_PANEL": { 23 | return { 24 | ...state, 25 | panels: [...state.panels, action.payload], 26 | }; 27 | } 28 | 29 | case "ADD_SETTING": { 30 | const sidebar_id = action.payload.path[0]; 31 | 32 | return { 33 | ...state, 34 | settings: [...state.settings, action.payload], 35 | sidebars: produce(state.sidebars, draft => { 36 | const sidebar = draft.find(({ id }) => id === sidebar_id); 37 | 38 | if (sidebar) { 39 | sidebar.settings_id = [ 40 | ...sidebar.settings_id, 41 | action.payload.id, 42 | ]; 43 | } 44 | }), 45 | }; 46 | } 47 | 48 | case "ADD_SIDEBAR": { 49 | return { 50 | ...state, 51 | sidebars: [...state.sidebars, action.payload], 52 | }; 53 | } 54 | 55 | case "ADD_TAB": { 56 | return { 57 | ...state, 58 | tabs: [...state.tabs, action.payload], 59 | }; 60 | } 61 | 62 | case "OPEN_TAB": { 63 | const { sidebar_id, tab_id } = action.payload; 64 | 65 | return { 66 | ...state, 67 | sidebars: produce(state.sidebars, draft => { 68 | const sidebar = draft.find(({ id }) => id === sidebar_id); 69 | 70 | if (sidebar) { 71 | sidebar.active_tab = tab_id; 72 | } 73 | }), 74 | }; 75 | } 76 | 77 | case "SET_META_KEY_EXISTS": { 78 | return produce(state, draft_state => { 79 | draft_state.settings.forEach(setting => { 80 | if (setting.data_key_with_prefix === action.payload) { 81 | setting.meta_key_exists = true; 82 | } 83 | }); 84 | }); 85 | } 86 | 87 | case "UPDATE_PROP_NONE": { 88 | const { setting_id, value } = action.payload; 89 | 90 | return { 91 | ...state, 92 | settings_none: { 93 | ...state.settings_none, 94 | [setting_id]: value, 95 | }, 96 | }; 97 | } 98 | 99 | case "UPDATE_PROP_LOCAL": { 100 | const { setting_id, value } = action.payload; 101 | 102 | return produce(state, draft_state => { 103 | const setting = draft_state.settings.find( 104 | ({ id }) => id === setting_id 105 | ); 106 | 107 | if (!setting) { 108 | return; 109 | } 110 | 111 | draft_state.settings_persisted[ 112 | setting.data_key_with_prefix 113 | ] = value; 114 | }); 115 | } 116 | 117 | default: 118 | return state; 119 | } 120 | }; 121 | -------------------------------------------------------------------------------- /src/store/selectors.ts: -------------------------------------------------------------------------------- 1 | import { last, flatten } from "lodash"; 2 | 3 | export const selectors: Selectors = { 4 | getSettingsAll: state => state.settings, 5 | getActiveTab: (state, sidebar_id) => { 6 | const sidebar = state.sidebars.find(({ id }) => id === sidebar_id); 7 | 8 | if (!sidebar) { 9 | return ""; 10 | } 11 | 12 | return sidebar.active_tab; 13 | }, 14 | getSidebar: (state, id) => 15 | state.sidebars.find(sidebar => sidebar.id === id), 16 | getSettings: (state, panel_id) => 17 | state.settings.filter(setting => last(setting.path) === panel_id), 18 | getPanels: (state, tab_id) => 19 | state.panels.filter(panel => last(panel.path) === tab_id), 20 | getTabs: (state, sidebar_id) => 21 | state.tabs.filter(tab => last(tab.path) === sidebar_id), 22 | getSettingsPersisted: state => state.settings_persisted, 23 | getSettingsNone: state => state.settings_none, 24 | getWarnings: (state, sidebar_id) => { 25 | const sidebars = state.sidebars.filter(({ id }) => id === sidebar_id); 26 | 27 | // If there is more than one sidebar returned it means two or more sidebars 28 | // share the same id. In that case we only return this warning. Otherwise, 29 | // the warnings from one sidebar's elements would be shared with the duplicate one. 30 | if (sidebars.length > 1) { 31 | return sidebars[1].warnings; 32 | } 33 | 34 | const tabs = state.tabs.filter(({ path }) => path[0] === sidebar_id); 35 | const panels = state.panels.filter( 36 | ({ path }) => path[0] === sidebar_id 37 | ); 38 | const settings = state.settings.filter( 39 | ({ path }) => path[0] === sidebar_id 40 | ); 41 | 42 | return flatten( 43 | [...sidebars, ...tabs, ...panels, ...settings].map( 44 | (item: SidebarProps | TabProps | PanelProps | SettingProps) => 45 | item.warnings 46 | ) 47 | ); 48 | }, 49 | }; 50 | -------------------------------------------------------------------------------- /src/utils/components/A.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { prepareProps } from "@/utils/tools/prepareProps"; 3 | 4 | export const A: React.ComponentType = props => { 5 | const { children, ...rest } = props; 6 | 7 | return {children}; 8 | }; 9 | -------------------------------------------------------------------------------- /src/utils/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { prepareProps } from "@/utils/tools/prepareProps"; 3 | 4 | export const Button: React.ComponentType = props => { 5 | const { children, ...rest } = props; 6 | 7 | return ( 8 | 11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /src/utils/components/Div.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { prepareProps } from "@/utils/tools/prepareProps"; 3 | 4 | export const Div: React.ComponentType = props => { 5 | const { children, ...rest } = props; 6 | 7 | return
{children}
; 8 | }; 9 | -------------------------------------------------------------------------------- /src/utils/components/H3.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { prepareProps } from "@/utils/tools/prepareProps"; 3 | 4 | export const H3: React.ComponentType = props => { 5 | const { children, ...rest } = props; 6 | 7 | return

{children}

; 8 | }; 9 | -------------------------------------------------------------------------------- /src/utils/components/Icon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Fragment } from "@wordpress/element"; 3 | 4 | import { Icons, icons } from "@/utils/data/icons"; 5 | 6 | interface Props { 7 | icon: keyof Icons; 8 | } 9 | 10 | export const Icon: React.ComponentType = props => { 11 | const { icon } = props; 12 | 13 | return {icon && icons[icon] ? icons[icon] : null}; 14 | }; 15 | -------------------------------------------------------------------------------- /src/utils/components/Img.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { prepareProps } from "@/utils/tools/prepareProps"; 3 | 4 | export const Img: React.ComponentType = props => { 5 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 6 | const { children, ...rest } = props; 7 | 8 | return ; 9 | }; 10 | -------------------------------------------------------------------------------- /src/utils/components/Li.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { prepareProps } from "@/utils/tools/prepareProps"; 3 | 4 | export const Li: React.ComponentType = props => { 5 | const { children, ...rest } = props; 6 | 7 | return
  • {children}
  • ; 8 | }; 9 | -------------------------------------------------------------------------------- /src/utils/components/Ol.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { prepareProps } from "@/utils/tools/prepareProps"; 3 | 4 | export const Ol: React.ComponentType = props => { 5 | const { children, ...rest } = props; 6 | 7 | return
      {children}
    ; 8 | }; 9 | -------------------------------------------------------------------------------- /src/utils/components/P.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { prepareProps } from "@/utils/tools/prepareProps"; 3 | 4 | export const P: React.ComponentType = props => { 5 | const { children, ...rest } = props; 6 | 7 | return

    {children}

    ; 8 | }; 9 | -------------------------------------------------------------------------------- /src/utils/components/Span.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { prepareProps } from "@/utils/tools/prepareProps"; 3 | 4 | export const Span: React.ComponentType = props => { 5 | const { children, ...rest } = props; 6 | 7 | return {children}; 8 | }; 9 | -------------------------------------------------------------------------------- /src/utils/components/Ul.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { prepareProps } from "@/utils/tools/prepareProps"; 3 | 4 | export const Ul: React.ComponentType = props => { 5 | const { children, ...rest } = props; 6 | 7 | return
      {children}
    ; 8 | }; 9 | -------------------------------------------------------------------------------- /src/utils/components/index.ts: -------------------------------------------------------------------------------- 1 | export { A } from "./A"; 2 | export { Button } from "./Button"; 3 | export { Div } from "./Div"; 4 | export { H3 } from "./H3"; 5 | export { Icon } from "./Icon"; 6 | export { Img } from "./Img"; 7 | export { Li } from "./Li"; 8 | export { Ol } from "./Ol"; 9 | export { P } from "./P"; 10 | export { Span } from "./Span"; 11 | export { Ul } from "./Ul"; 12 | -------------------------------------------------------------------------------- /src/utils/data/icons.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | export type Icons = Record<"remove" | "add", JSX.Element>; 3 | 4 | export const icons: Icons = { 5 | remove: ( 6 | /* https://material.io/tools/icons/?icon=remove_circle */ 7 | 8 | 9 | 10 | 11 | ), 12 | add: ( 13 | /* https://material.io/tools/icons/?icon=add_circle */ 14 | 15 | 16 | 17 | 18 | ), 19 | }; 20 | -------------------------------------------------------------------------------- /src/utils/data/index.ts: -------------------------------------------------------------------------------- 1 | export { plugin_namespace, plugin_prefix, store_slug } from "./plugin"; 2 | export { icons } from "./icons"; 3 | -------------------------------------------------------------------------------- /src/utils/data/plugin.ts: -------------------------------------------------------------------------------- 1 | export const plugin_namespace = "post-meta-controls"; 2 | export const plugin_prefix = "pmc"; 3 | export const store_slug = `melonpan/${plugin_namespace}`; 4 | -------------------------------------------------------------------------------- /src/utils/data/stylus_variables.styl: -------------------------------------------------------------------------------- 1 | $e_standard = cubic-bezier(0.4, 0.0, 0.2, 1) 2 | $e_deceleration = cubic-bezier(0.0, 0.0, 0.2, 1) 3 | -------------------------------------------------------------------------------- /src/utils/tools/addPrefix.ts: -------------------------------------------------------------------------------- 1 | import { compact, flow, isString } from "lodash"; 2 | 3 | import { plugin_prefix } from "@/utils/data/plugin"; 4 | 5 | const resolvePrefix = ( 6 | element: string, 7 | separator: string, 8 | prefix: string 9 | ): string => { 10 | if (element.startsWith("!")) { 11 | return element.replace("!", ""); 12 | } 13 | 14 | return prefix + separator + element; 15 | }; 16 | 17 | export const addPrefix = ( 18 | elements: string | null | (string | null)[] | undefined, 19 | separator = "-", 20 | prefix = plugin_prefix 21 | ): string => { 22 | if (!elements) { 23 | return ""; 24 | } 25 | 26 | if (isString(elements)) { 27 | return resolvePrefix(elements, separator, prefix); 28 | } 29 | 30 | return flow([ 31 | compact, 32 | (elements: string[]) => 33 | elements.map(el => resolvePrefix(el, separator, prefix)), 34 | (elements: string[]) => elements.join(" "), 35 | ])(elements); 36 | }; 37 | -------------------------------------------------------------------------------- /src/utils/tools/castSchema.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // TODO 3 | import { 4 | isUndefined, 5 | isObject, 6 | isArray, 7 | castArray, 8 | forEach, 9 | get, 10 | } from "lodash"; 11 | 12 | import { sanitize } from "@/utils/tools/sanitize.ts"; 13 | 14 | export const castSchema = (elements, schema) => { 15 | forEach(elements, (value, key) => { 16 | let schema_type; 17 | 18 | if (!isUndefined(get(schema[key], "type"))) { 19 | schema_type = schema[key].type; 20 | } else if (!isUndefined(schema[key])) { 21 | schema_type = schema[key]; 22 | } else if (!isUndefined(schema._all)) { 23 | schema_type = schema._all; 24 | } else { 25 | delete elements[key]; 26 | return; 27 | } 28 | 29 | if (isObject(schema_type)) { 30 | value = 31 | !isObject(value) && !isArray(value) ? castArray(value) : value; 32 | 33 | elements[key] = castSchema(value, schema_type); 34 | return; 35 | } 36 | 37 | switch (schema_type) { 38 | case "html": 39 | if (value === "") { 40 | elements[key] = ""; 41 | break; 42 | } 43 | 44 | value = JSON.parse(value); 45 | elements[key] = sanitize.html(value); 46 | break; 47 | 48 | case "id": 49 | elements[key] = sanitize.id(value); 50 | break; 51 | 52 | case "url": 53 | elements[key] = sanitize.url(value); 54 | break; 55 | 56 | case "text": 57 | elements[key] = sanitize.text(value); 58 | break; 59 | 60 | case "integer": 61 | elements[key] = sanitize.integer(value); 62 | break; 63 | 64 | case "float": 65 | elements[key] = sanitize.float(value); 66 | break; 67 | 68 | case "boolean": 69 | elements[key] = sanitize.boolean(value); 70 | break; 71 | 72 | default: 73 | elements[key] = ""; 74 | break; 75 | } 76 | }); 77 | 78 | return elements; 79 | }; 80 | -------------------------------------------------------------------------------- /src/utils/tools/getColorScheme.ts: -------------------------------------------------------------------------------- 1 | export const getColorScheme = ( 2 | color_scheme: string 3 | ): { id: string; type: string } => { 4 | switch (color_scheme) { 5 | case "light": 6 | return { 7 | id: "", 8 | type: "light", 9 | }; 10 | 11 | case "banana": 12 | return { 13 | id: "banana", 14 | type: "light", 15 | }; 16 | 17 | case "melon": 18 | return { 19 | id: "melon", 20 | type: "light", 21 | }; 22 | 23 | case "melocoton": 24 | return { 25 | id: "melocoton", 26 | type: "light", 27 | }; 28 | 29 | case "coco": 30 | return { 31 | id: "coco", 32 | type: "light", 33 | }; 34 | 35 | case "mandarina": 36 | return { 37 | id: "mandarina", 38 | type: "light", 39 | }; 40 | 41 | case "pistacho": 42 | return { 43 | id: "pistacho", 44 | type: "light", 45 | }; 46 | 47 | case "dark": 48 | return { 49 | id: "plain_dark", 50 | type: "dark", 51 | }; 52 | 53 | case "higo": 54 | return { 55 | id: "higo", 56 | type: "dark", 57 | }; 58 | 59 | case "mango": 60 | return { 61 | id: "mango", 62 | type: "dark", 63 | }; 64 | 65 | case "endrina": 66 | return { 67 | id: "endrina", 68 | type: "dark", 69 | }; 70 | 71 | case "castana": 72 | return { 73 | id: "castana", 74 | type: "dark", 75 | }; 76 | 77 | case "naranja": 78 | return { 79 | id: "naranja", 80 | type: "dark", 81 | }; 82 | 83 | case "ciruela": 84 | return { 85 | id: "ciruela", 86 | type: "dark", 87 | }; 88 | 89 | default: 90 | return { 91 | id: "", 92 | type: "light", 93 | }; 94 | } 95 | }; 96 | -------------------------------------------------------------------------------- /src/utils/tools/index.ts: -------------------------------------------------------------------------------- 1 | export { addPrefix } from "./addPrefix"; 2 | export { castSchema } from "./castSchema"; 3 | export { getColorScheme } from "./getColorScheme"; 4 | export { prepareIcon } from "./prepareIcon"; 5 | export { 6 | prepareImageDataFromMedia, 7 | prepareImageDataFromRest, 8 | } from "./prepareImageData"; 9 | export { prepareOptions } from "./prepareOptions"; 10 | export { prepareProps } from "./prepareProps"; 11 | export { sanitize } from "./sanitize"; 12 | -------------------------------------------------------------------------------- /src/utils/tools/prepareIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import DOMPurify from "dompurify"; 3 | import { RawHTML } from "@wordpress/element"; 4 | import { Dashicon } from "@wordpress/components"; 5 | 6 | import { Div } from "@/utils/components"; 7 | import { addPrefix } from "@/utils/tools/addPrefix"; 8 | 9 | export const prepareIcon = ( 10 | icon_svg: string, 11 | icon_dashicon: string, 12 | type: "tab" | "panel" 13 | ): React.ReactNode => { 14 | if (icon_svg === "" && icon_dashicon === "") { 15 | return null; 16 | } 17 | 18 | if (icon_svg !== "") { 19 | return ( 20 | 21 | {DOMPurify.sanitize(icon_svg)} 22 | 23 | ); 24 | } 25 | 26 | return ( 27 |
    28 | 29 |
    30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /src/utils/tools/prepareImageData.ts: -------------------------------------------------------------------------------- 1 | export const prepareImageDataFromMedia = ( 2 | images_raw: ImageFromMedia[] 3 | ): Image[] => 4 | images_raw.reduce((data, image_raw) => { 5 | const { id, alt, sizes } = image_raw; 6 | 7 | const { url } = 8 | sizes.medium_large || 9 | sizes.medium || 10 | sizes.large || 11 | sizes.thumbnail || 12 | sizes.full; 13 | 14 | return data.concat({ 15 | id, 16 | alt, 17 | url, 18 | }); 19 | }, []); 20 | 21 | export const prepareImageDataFromRest = ( 22 | images_raw: ImageFromRest[] 23 | ): Image[] => 24 | images_raw.reduce((data, image_raw) => { 25 | const { 26 | id, 27 | alt_text: alt, 28 | media_details: { sizes }, 29 | } = image_raw; 30 | 31 | const { source_url } = 32 | sizes.medium_large || 33 | sizes.medium || 34 | sizes.large || 35 | sizes.thumbnail || 36 | sizes.full || 37 | image_raw; 38 | 39 | return data.concat({ 40 | id, 41 | alt, 42 | url: source_url, 43 | }); 44 | }, []); 45 | -------------------------------------------------------------------------------- /src/utils/tools/prepareOptions.ts: -------------------------------------------------------------------------------- 1 | import { reduce, isArray, isUndefined } from "lodash"; 2 | 3 | interface OptionsRaw { 4 | [value: string]: string; 5 | } 6 | 7 | interface Option { 8 | value: string; 9 | label: string; 10 | } 11 | 12 | export const prepareOptions = ( 13 | options: Option[] | OptionsRaw | undefined 14 | ): Option[] => { 15 | if (isUndefined(options)) { 16 | return []; 17 | } 18 | 19 | if (isArray(options)) { 20 | return options; 21 | } 22 | 23 | return reduce( 24 | options, 25 | (acc, value, key) => 26 | acc.concat({ 27 | value: key, 28 | label: value, 29 | }), 30 | [] 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /src/utils/tools/prepareProps.ts: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren } from "react"; 2 | import { addPrefix } from "@/utils/tools/addPrefix"; 3 | 4 | export const prepareProps = ( 5 | props: ComponentProps 6 | ): PropsWithChildren> => { 7 | const { id, className, ...rest } = props; 8 | 9 | return { 10 | id: addPrefix(id) || undefined, 11 | className: addPrefix(className) || undefined, 12 | ...rest, 13 | }; 14 | }; 15 | -------------------------------------------------------------------------------- /src/utils/tools/sanitize.ts: -------------------------------------------------------------------------------- 1 | import DOMPurify from "dompurify"; 2 | import { 3 | isString, 4 | isArray, 5 | isBoolean, 6 | toSafeInteger, 7 | toNumber, 8 | deburr, 9 | } from "lodash"; 10 | 11 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types 12 | const sanitizeHtml = (value: any): string => { 13 | if (!isString(value)) { 14 | return ""; 15 | } 16 | 17 | value = DOMPurify.sanitize(value); 18 | 19 | return value; 20 | }; 21 | 22 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types 23 | const sanitizeUrl = (value: any) => { 24 | if (!isString(value)) { 25 | return "#"; 26 | } 27 | 28 | /* https://stackoverflow.com/a/3809435 | CC BY-SA 3.0 */ 29 | const url_regex = new RegExp( 30 | // eslint-disable-next-line no-useless-escape 31 | /[-a-zA-Z0-9@:%_\+.~#?&//=]{2,256}\.[a-z]{2,4}\b(\/[-a-zA-Z0-9@:%_\+.~#?&//=]*)?/gi 32 | ); 33 | 34 | if (!value.match(url_regex)) { 35 | return "#"; 36 | } 37 | 38 | return value; 39 | }; 40 | 41 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types 42 | const sanitizeId = (value: any): string => { 43 | if (!isString(value)) { 44 | return ""; 45 | } 46 | 47 | value = deburr(value); 48 | value = value.replace(/[^\w-]/g, ""); 49 | 50 | return value; 51 | }; 52 | 53 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types 54 | const sanitizeText = (value: any) => { 55 | if (!isString(value)) { 56 | return ""; 57 | } 58 | 59 | const text = document.createTextNode(value); 60 | const paragraph = document.createElement("p"); 61 | 62 | paragraph.appendChild(text); 63 | 64 | return paragraph.innerHTML; 65 | }; 66 | 67 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types 68 | const sanitizeBoolean = (value: any) => { 69 | return isBoolean(value) && value === true ? true : false; 70 | }; 71 | 72 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types 73 | const sanitizeFloat = (value: any): number => { 74 | value = toNumber(value); 75 | value = Math.abs(value); 76 | value = Math.round(100 * value) / 100; 77 | 78 | return value; 79 | }; 80 | 81 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types 82 | const sanitizeInteger = (value: any): number => { 83 | value = toSafeInteger(value); 84 | value = Math.abs(value); 85 | 86 | return value; 87 | }; 88 | 89 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types 90 | const sanitizeArray = (value: any): T[] => { 91 | if (!isArray(value)) { 92 | return []; 93 | } 94 | 95 | return value; 96 | }; 97 | 98 | export const sanitize = { 99 | id: sanitizeId, 100 | url: sanitizeUrl, 101 | html: sanitizeHtml, 102 | text: sanitizeText, 103 | boolean: sanitizeBoolean, 104 | float: sanitizeFloat, 105 | integer: sanitizeInteger, 106 | array: sanitizeArray, 107 | }; 108 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "moduleResolution": "Node", 5 | "noEmit": true, 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "allowJs": true, 10 | "jsx": "react", 11 | "baseUrl": ".", 12 | "paths": { 13 | "@/*": ["src/*"] 14 | } 15 | }, 16 | "include": ["src", "types"], 17 | "exclude": ["_extras", "_release", "dist", "node_modules", "pro"] 18 | } 19 | -------------------------------------------------------------------------------- /types/Base.d.ts: -------------------------------------------------------------------------------- 1 | type BaseProps = PanelProps | SettingProps; 2 | 3 | type BasePropsRaw = BaseProps | Partial; 4 | 5 | type BasePropsPrivates = PanelPropsPrivates | SettingPropsPrivates; 6 | 7 | type BasePropsSchema = PanelPropsSchema | SettingPropsSchema; 8 | -------------------------------------------------------------------------------- /types/Panel.d.ts: -------------------------------------------------------------------------------- 1 | type PanelProps = { 2 | class_name: "panel"; 3 | warnings: Warning[]; 4 | id: string; 5 | path: string[]; 6 | label: string; 7 | initial_open: boolean; 8 | collapsible: boolean; 9 | icon_dashicon: string; 10 | icon_svg: string; 11 | }; 12 | 13 | type PanelPropsRaw = Partial>; 14 | 15 | type PanelPropsSchema = Record; 16 | 17 | type PanelPropsPrivates = ("class_name" | "warnings")[]; 18 | -------------------------------------------------------------------------------- /types/Setting-Buttons.d.ts: -------------------------------------------------------------------------------- 1 | type ButtonsOption = { 2 | value: string; 3 | title: string; 4 | icon_dashicon: string; 5 | icon_svg: string; 6 | }; 7 | 8 | type ButtonsProps = { 9 | type: "buttons"; 10 | default_value: string; 11 | allow_empty: boolean; 12 | options: ButtonsOption[]; 13 | }; 14 | 15 | type ButtonsPropsRaw = Partial> & { 16 | options?: Partial[]; 17 | }; 18 | 19 | type ButtonsPropsSchema = Record< 20 | keyof Omit, 21 | SchemaElement 22 | >; 23 | -------------------------------------------------------------------------------- /types/Setting-Checkbox.d.ts: -------------------------------------------------------------------------------- 1 | type CheckboxProps = { 2 | type: "checkbox"; 3 | default_value: boolean; 4 | input_label: string; 5 | use_toggle: boolean; 6 | }; 7 | 8 | type CheckboxPropsRaw = Partial>; 9 | 10 | type CheckboxPropsSchema = Record< 11 | keyof Omit, 12 | SchemaElement 13 | >; 14 | -------------------------------------------------------------------------------- /types/Setting-CheckboxMultiple.d.ts: -------------------------------------------------------------------------------- 1 | type CheckboxMultipleProps = { 2 | type: "checkbox_multiple"; 3 | default_value: string[]; 4 | options: { value: string; label: string }[]; 5 | use_toggle: boolean; 6 | }; 7 | 8 | type CheckboxMultiplePropsRaw = Partial>; 9 | 10 | type CheckboxMultiplePropsSchema = Record< 11 | keyof Omit, 12 | SchemaElement 13 | >; 14 | -------------------------------------------------------------------------------- /types/Setting-Color.d.ts: -------------------------------------------------------------------------------- 1 | type ColorPaletteRaw = { 2 | [color_name: string]: string; 3 | }; 4 | 5 | type ColorPalette = { 6 | name: string; 7 | color: string; 8 | }; 9 | 10 | type ColorProps = { 11 | type: "color"; 12 | default_value: string; 13 | alpha_control: boolean; 14 | palette: ColorPalette[]; 15 | }; 16 | 17 | type ColorPropsRaw = Partial> & { 18 | palette?: ColorPalette[] | ColorPaletteRaw; 19 | }; 20 | 21 | type ColorPropsSchema = Record, SchemaElement>; 22 | -------------------------------------------------------------------------------- /types/Setting-CustomText.d.ts: -------------------------------------------------------------------------------- 1 | type CustomTextProps = { 2 | type: "custom_text"; 3 | content: { type: string; content: string[]; href: string }[]; 4 | }; 5 | 6 | type CustomTextPropsRaw = Partial>; 7 | 8 | type CustomTextPropsSchema = Record< 9 | keyof Omit, 10 | SchemaElement 11 | >; 12 | -------------------------------------------------------------------------------- /types/Setting-DateRange.d.ts: -------------------------------------------------------------------------------- 1 | type DateRangeProps = { 2 | type: "date_range"; 3 | default_value: [string, string] | []; 4 | format: string; 5 | locale: string; 6 | unavailable_dates: [string, string][]; 7 | minimum_days: number; 8 | maximum_days: number; 9 | }; 10 | 11 | type DateRangePropsRaw = Partial>; 12 | 13 | type DateRangePropsSchema = Record< 14 | keyof Omit, 15 | SchemaElement 16 | >; 17 | -------------------------------------------------------------------------------- /types/Setting-DateSingle.d.ts: -------------------------------------------------------------------------------- 1 | type DateSingleProps = { 2 | type: "date_single"; 3 | default_value: string; 4 | format: string; 5 | locale: string; 6 | unavailable_dates: [string, string][]; 7 | }; 8 | 9 | type DateSinglePropsRaw = Partial>; 10 | 11 | type DateSinglePropsSchema = Record< 12 | keyof Omit, 13 | SchemaElement 14 | >; 15 | -------------------------------------------------------------------------------- /types/Setting-Image.d.ts: -------------------------------------------------------------------------------- 1 | type ImageProps = { 2 | type: "image"; 3 | default_value: number; 4 | }; 5 | 6 | type ImagePropsRaw = Partial>; 7 | 8 | type ImagePropsSchema = Record, SchemaElement>; 9 | -------------------------------------------------------------------------------- /types/Setting-ImageMultiple.d.ts: -------------------------------------------------------------------------------- 1 | type ImageMultipleProps = { 2 | type: "image_multiple"; 3 | default_value: number[]; 4 | }; 5 | 6 | type ImageMultiplePropsRaw = Partial>; 7 | 8 | type ImageMultiplePropsSchema = Record< 9 | keyof Omit, 10 | SchemaElement 11 | >; 12 | -------------------------------------------------------------------------------- /types/Setting-Radio.d.ts: -------------------------------------------------------------------------------- 1 | type RadioOption = { 2 | value: string; 3 | label: string; 4 | }; 5 | 6 | type RadioProps = { 7 | type: "radio"; 8 | default_value: string; 9 | options: RadioOption[]; 10 | }; 11 | 12 | type RadioPropsRaw = Partial>; 13 | 14 | type RadioPropsSchema = Record, SchemaElement>; 15 | -------------------------------------------------------------------------------- /types/Setting-Range.d.ts: -------------------------------------------------------------------------------- 1 | type RangeProps = { 2 | type: "range"; 3 | default_value: number; 4 | step: number; 5 | min: number; 6 | max: number; 7 | }; 8 | 9 | type RangePropsRaw = Partial>; 10 | 11 | type RangePropsSchema = Record, SchemaElement>; 12 | -------------------------------------------------------------------------------- /types/Setting-RangeFloat.d.ts: -------------------------------------------------------------------------------- 1 | type RangeFloatProps = { 2 | type: "range_float"; 3 | default_value: number; 4 | step: number; 5 | min: number; 6 | max: number; 7 | }; 8 | 9 | type RangeFloatPropsRaw = Partial>; 10 | 11 | type RangeFloatPropsSchema = Record< 12 | keyof Omit, 13 | SchemaElement 14 | >; 15 | -------------------------------------------------------------------------------- /types/Setting-Select.d.ts: -------------------------------------------------------------------------------- 1 | type SelectOption = { 2 | value: string; 3 | label: string; 4 | }; 5 | 6 | type SelectProps = { 7 | type: "select"; 8 | default_value: string; 9 | options: SelectOption[]; 10 | }; 11 | 12 | type SelectPropsRaw = Partial>; 13 | 14 | type SelectPropsSchema = Record, SchemaElement>; 15 | -------------------------------------------------------------------------------- /types/Setting-Text.d.ts: -------------------------------------------------------------------------------- 1 | type TextProps = { 2 | type: "text"; 3 | default_value: string; 4 | placeholder: string; 5 | }; 6 | 7 | type TextPropsRaw = Partial>; 8 | 9 | type TextPropsSchema = Record, SchemaElement>; 10 | -------------------------------------------------------------------------------- /types/Setting-Textarea.d.ts: -------------------------------------------------------------------------------- 1 | type TextareaProps = { 2 | type: "textarea"; 3 | default_value: string; 4 | placeholder: string; 5 | }; 6 | 7 | type TextareaPropsRaw = Partial>; 8 | 9 | type TextareaPropsSchema = Record< 10 | keyof Omit, 11 | SchemaElement 12 | >; 13 | -------------------------------------------------------------------------------- /types/Setting.d.ts: -------------------------------------------------------------------------------- 1 | type SettingProps = SettingPropsShared & SettingPropsReceived; 2 | 3 | type SettingPropsReceived = 4 | | ButtonsProps 5 | | CheckboxProps 6 | | CheckboxMultipleProps 7 | | ColorProps 8 | | CustomTextProps 9 | | DateRangeProps 10 | | DateSingleProps 11 | | ImageProps 12 | | ImageMultipleProps 13 | | RadioProps 14 | | RangeProps 15 | | RangeFloatProps 16 | | SelectProps 17 | | TextProps 18 | | TextareaProps; 19 | 20 | type SettingPropsRawReceived = Partial< 21 | Omit 22 | > & 23 | ( 24 | | ButtonsPropsRaw 25 | | CheckboxPropsRaw 26 | | CheckboxMultiplePropsRaw 27 | | ColorPropsRaw 28 | | CustomTextPropsRaw 29 | | DateRangePropsRaw 30 | | DateSinglePropsRaw 31 | | ImagePropsRaw 32 | | ImageMultiplePropsRaw 33 | | RadioPropsRaw 34 | | RangePropsRaw 35 | | RangeFloatPropsRaw 36 | | SelectPropsRaw 37 | | TextPropsRaw 38 | | TextareaPropsRaw 39 | ); 40 | 41 | type SettingPropsRaw = Partial & 42 | ( 43 | | ButtonsPropsRaw 44 | | CheckboxPropsRaw 45 | | CheckboxMultiplePropsRaw 46 | | ColorPropsRaw 47 | | CustomTextPropsRaw 48 | | DateRangePropsRaw 49 | | DateSinglePropsRaw 50 | | ImagePropsRaw 51 | | ImageMultiplePropsRaw 52 | | RadioPropsRaw 53 | | RangePropsRaw 54 | | RangeFloatPropsRaw 55 | | SelectPropsRaw 56 | | TextPropsRaw 57 | | TextareaPropsRaw 58 | ); 59 | 60 | type SettingPropsShared = { 61 | class_name: "setting"; 62 | warnings: Warning[]; 63 | meta_key_exists: boolean; 64 | data_key_with_prefix: string; 65 | id: string; 66 | path: string[]; 67 | label: string; 68 | help: string; 69 | data_type: "none" | "meta" | "localstorage"; 70 | ui_border_top: boolean; 71 | }; 72 | 73 | type SettingPropsPrivates = ("class_name" | "warnings")[]; 74 | 75 | type SettingPropsSchema = Record; 76 | 77 | type SettingPropsSchemaReceived = 78 | | ButtonsPropsSchema 79 | | CheckboxPropsSchema 80 | | CheckboxMultiplePropsSchema 81 | | ColorPropsSchema 82 | | CustomTextPropsSchema 83 | | DateRangePropsSchema 84 | | DateSinglePropsSchema 85 | | ImagePropsSchema 86 | | ImageMultiplePropsSchema 87 | | RadioPropsSchema 88 | | RangePropsSchema 89 | | RangeFloatPropsSchema 90 | | SelectPropsSchema 91 | | TextPropsSchema 92 | | TextareaPropsSchema; 93 | -------------------------------------------------------------------------------- /types/Sidebar.d.ts: -------------------------------------------------------------------------------- 1 | type SidebarProps = { 2 | class_name: "sidebar"; 3 | warnings: Warning[]; 4 | id: string; 5 | label: string; 6 | active_tab: string; 7 | settings_id: SettingProps["id"][]; 8 | icon_dashicon: string; 9 | icon_svg: string; 10 | id_already_exists: boolean; 11 | ui_color_scheme: "light"; 12 | }; 13 | 14 | type SidebarPropsRaw = Partial>; 15 | 16 | type SidebarPropsSchema = Record; 17 | 18 | type SidebarPropsPrivates = ("class_name" | "warnings")[]; 19 | -------------------------------------------------------------------------------- /types/Tab.d.ts: -------------------------------------------------------------------------------- 1 | type TabProps = { 2 | class_name: "tab"; 3 | warnings: Warning[]; 4 | id: string; 5 | path: string[]; 6 | label: string; 7 | icon_dashicon: string; 8 | icon_svg: string; 9 | }; 10 | 11 | type TabPropsRaw = Partial>; 12 | 13 | type TabPropsSchema = Record; 14 | 15 | type TabPropsPrivates = ("class_name" | "warnings")[]; 16 | -------------------------------------------------------------------------------- /types/others.d.ts: -------------------------------------------------------------------------------- 1 | interface ComponentProps extends Obj { 2 | children?: React.ReactNode; 3 | id?: string | null; 4 | className?: string | null | (string | null)[] | undefined; 5 | } 6 | 7 | type SanitizeType = 8 | | "html" 9 | | "id" 10 | | "url" 11 | | "text" 12 | | "integer" 13 | | "float" 14 | | "boolean"; 15 | 16 | type SanitizeTypeOrObject = 17 | | SanitizeType 18 | | { 19 | [key: string]: SanitizeType | SanitizeTypeOrObject; 20 | } 21 | | { 22 | _all: SanitizeType | SanitizeTypeOrObject; 23 | } 24 | | { 25 | type: SanitizeType | SanitizeTypeOrObject; 26 | }; 27 | 28 | type SchemaConditionObject = { value: boolean; message: string }; 29 | 30 | type SchemaCondition = 31 | | false 32 | | "not_empty" 33 | | SchemaConditionObject 34 | | SchemaConditionObject[]; 35 | 36 | type SchemaElement = { 37 | type: SanitizeTypeOrObject; 38 | conditions?: SchemaCondition; 39 | }; 40 | 41 | interface Schema { 42 | [prop: string]: SchemaElement; 43 | } 44 | 45 | type Warning = { 46 | title: string; 47 | message: string; 48 | }; 49 | 50 | type ImageSize = { 51 | source_url: string; 52 | url: string; 53 | }; 54 | 55 | type ImageSizes = Record; 56 | 57 | type ImageFromMedia = { 58 | id: number; 59 | alt: string; 60 | sizes: ImageSizes; 61 | }; 62 | 63 | type ImageFromRest = { 64 | id: number; 65 | alt_text: string; 66 | media_details: { 67 | sizes: ImageSizes; 68 | }; 69 | source_url: string; 70 | }; 71 | 72 | type Image = { 73 | id: number; 74 | alt: string; 75 | url: string; 76 | }; 77 | 78 | type ItemProps = SidebarProps | TabProps | PanelProps | SettingProps; 79 | -------------------------------------------------------------------------------- /types/store-actions.d.ts: -------------------------------------------------------------------------------- 1 | type Action = { 2 | type: T; 3 | payload: P; 4 | }; 5 | 6 | type ActionCreator = { 7 | (payload: A["payload"]): A; 8 | }; 9 | 10 | type ActionUpdatePropLocal = Action< 11 | "UPDATE_PROP_LOCAL", 12 | { 13 | setting_id: SettingProps["id"]; 14 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 15 | value: any; 16 | } 17 | >; 18 | type ActionUpdatePropNone = Action< 19 | "UPDATE_PROP_NONE", 20 | { 21 | setting_id: SettingProps["id"]; 22 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 23 | value: any; 24 | } 25 | >; 26 | type ActionSetMetaKeyExists = Action<"SET_META_KEY_EXISTS", string>; 27 | type ActionOpenTab = Action< 28 | "OPEN_TAB", 29 | { sidebar_id: SidebarProps["id"]; tab_id: TabProps["id"] } 30 | >; 31 | type ActionAddSidebar = Action<"ADD_SIDEBAR", SidebarProps>; 32 | type ActionAddTab = Action<"ADD_TAB", TabProps>; 33 | type ActionAddPanel = Action<"ADD_PANEL", PanelProps>; 34 | //eslint-disable-next-line @typescript-eslint/no-explicit-any 35 | type ActionAddSetting = Action<"ADD_SETTING", SettingProps & { value: any }>; 36 | 37 | type ActionCreators = { 38 | updatePropLocal: ActionCreator; 39 | updatePropNone: ActionCreator; 40 | setMetaKeyExists: ActionCreator; 41 | openTab: ActionCreator; 42 | addSidebar: ActionCreator; 43 | addTab: ActionCreator; 44 | addPanel: ActionCreator; 45 | addSetting: ActionCreator; 46 | }; 47 | 48 | type Actions = 49 | | ActionUpdatePropLocal 50 | | ActionUpdatePropNone 51 | | ActionSetMetaKeyExists 52 | | ActionOpenTab 53 | | ActionAddSidebar 54 | | ActionAddTab 55 | | ActionAddPanel 56 | | ActionAddSetting; 57 | -------------------------------------------------------------------------------- /types/store-selectors.d.ts: -------------------------------------------------------------------------------- 1 | type Selector = (state: State, parameter: P) => T; 2 | 3 | type Selectors = { 4 | getSettingsAll: Selector; 5 | getActiveTab: Selector; 6 | getSidebar: Selector; 7 | getSettings: Selector; 8 | getPanels: Selector; 9 | getTabs: Selector; 10 | getSettingsPersisted: Selector; 11 | getSettingsNone: Selector; 12 | getWarnings: Selector; 13 | }; 14 | -------------------------------------------------------------------------------- /types/store-state.d.ts: -------------------------------------------------------------------------------- 1 | type State = { 2 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 3 | settings: (SettingProps & { value: any })[]; 4 | panels: PanelProps[]; 5 | tabs: TabProps[]; 6 | sidebars: SidebarProps[]; 7 | settings_persisted: Obj; 8 | settings_none: Obj; 9 | }; 10 | -------------------------------------------------------------------------------- /types/utils.d.ts: -------------------------------------------------------------------------------- 1 | interface Obj { 2 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 3 | [key: string]: any; 4 | } 5 | 6 | type SetState = (state: Partial) => void; 7 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const { 2 | name: short_name, 3 | description: name, 4 | version, 5 | homepage, 6 | } = require("./package.json"); 7 | const { BannerPlugin } = require("webpack"); 8 | const CssMinimizerPlugin = require("css-minimizer-webpack-plugin"); 9 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 10 | const TerserPlugin = require("terser-webpack-plugin"); 11 | const path = require("path"); 12 | 13 | module.exports = (env, { mode }) => { 14 | const is_production = mode === "production"; 15 | const is_development = !is_production; 16 | 17 | const config = { 18 | watch: is_development, 19 | 20 | entry: { 21 | [short_name]: path.resolve(__dirname, "src/entry.ts"), 22 | 23 | [`${short_name}-moment-locales`]: path.resolve( 24 | __dirname, 25 | "node_modules/moment/min/locales.js?pmc" 26 | ), 27 | }, 28 | 29 | output: { 30 | path: path.resolve(__dirname, "dist"), 31 | filename: "[name].js", 32 | }, 33 | 34 | resolve: { 35 | alias: { 36 | "@": path.resolve(__dirname, "src"), 37 | }, 38 | }, 39 | 40 | externals: { 41 | lodash: "lodash", 42 | moment: "moment", 43 | react: "React", 44 | "react-dom": "ReactDOM", 45 | "@wordpress/api-fetch": "wp.apiFetch", 46 | "@wordpress/block-editor": "wp.blockEditor", 47 | "@wordpress/components": "wp.components", 48 | "@wordpress/compose": "wp.compose", 49 | "@wordpress/data": "wp.data", 50 | "@wordpress/edit-post": "wp.editPost", 51 | "@wordpress/editor": "wp.editor", 52 | "@wordpress/element": "wp.element", 53 | "@wordpress/hooks": "wp.hooks", 54 | "@wordpress/i18n": "wp.i18n", 55 | "@wordpress/plugins": "wp.plugins", 56 | "@wordpress/url": "wp.url", 57 | }, 58 | 59 | module: { rules: [] }, 60 | 61 | plugins: [], 62 | }; 63 | 64 | config.module.rules.push({ 65 | test: /\.tsx?$/, 66 | exclude: /node_modules/, 67 | loader: "babel-loader", 68 | resolve: { 69 | extensions: [".ts", ".tsx", ".js", ".jsx"], 70 | }, 71 | }); 72 | 73 | config.module.rules.push({ 74 | test: /node_modules\/moment.+\.js?$/, 75 | loader: path.resolve(__dirname, "scripts/webpack_loader-moment"), 76 | resourceQuery: /pmc/, 77 | }); 78 | 79 | config.module.rules.push({ 80 | test: /\.(css|styl)$/, 81 | use: [MiniCssExtractPlugin.loader, "css-loader", "stylus-loader"], 82 | }); 83 | 84 | config.plugins.push( 85 | new MiniCssExtractPlugin({ 86 | filename: "[name].css", 87 | }) 88 | ); 89 | 90 | if (is_production) { 91 | config.plugins.push( 92 | new BannerPlugin({ 93 | banner: `${name} v${version} | ${homepage}`, 94 | include: /\.css/, 95 | }) 96 | ); 97 | 98 | config.plugins.push( 99 | new BannerPlugin({ 100 | banner: [ 101 | `${name} v${version} | ${homepage}`, 102 | "TinyColor | https://github.com/bgrins/TinyColor | 2016-07-07, Brian Grinstead | MIT License", 103 | "array-move | https://github.com/sindresorhus/array-move | Sindre Sorhus | MIT License", 104 | "DOMPurify | https://github.com/cure53/DOMPurify | Mario Heiderich | MPL-2.0 OR Apache-2.0", 105 | "immer | https://github.com/mweststrate/immer | Michel Weststrate | MIT License", 106 | "moment | http://momentjs.com | Iskren Ivov Chernev | MIT License", 107 | "react-dates | https://github.com/airbnb/react-dates#readme | Maja Wichrowska | MIT License", 108 | "react-sortable-hoc | https://github.com/clauderic/react-sortable-hoc | Clauderic Demers | MIT License", 109 | "uuid | https://github.com/kelektiv/node-uuid | MIT License", 110 | ].join("\n"), 111 | include: new RegExp(`${short_name}.js$`), 112 | }) 113 | ); 114 | 115 | config.plugins.push( 116 | new BannerPlugin({ 117 | banner: [ 118 | `${name} v${version} | ${homepage}`, 119 | `Moment.js | https://github.com/moment/moment | Iskren Ivov Chernev | MIT License`, 120 | ].join("\n"), 121 | include: new RegExp(`${short_name}-moment-locales.js$`), 122 | }) 123 | ); 124 | 125 | config.optimization = { 126 | minimize: true, 127 | minimizer: [ 128 | new CssMinimizerPlugin(), 129 | 130 | // As we are using a custom optimization, making use of 131 | // CssMinimizerPlugin, we also need to specify TerserPlugin 132 | new TerserPlugin({ extractComments: false }), 133 | ], 134 | }; 135 | } 136 | 137 | return config; 138 | }; 139 | --------------------------------------------------------------------------------