added in vendor for prod

This commit is contained in:
2025-05-06 08:24:59 -07:00
parent 6207303b1c
commit db52cd6933
91 changed files with 13191 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
vendor/
composer.lock
.php.result.cache

View File

@@ -0,0 +1 @@
{"version":1,"defects":{"ofc\\tests\\UtilTest::weCanSlugifyAString":3},"times":{"ofc\\tests\\UtilTest::weCanSlugifyAString":0.002}}

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Open Function Computers, LLC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,41 @@
<p align="center">
<img src="/logo.png" alt="ofc-logo" style="max-width:500px;" />
</p>
A suite of utilities to deliver a faster and more consistent WordPress theme development experience.
<br><br>
[![Latest Stable Version](https://poser.pugx.org/open-function-computers-llc/rad-theme-engine/v/stable.svg)](https://packagist.org/packages/open-function-computers-llc/rad-theme-engine) [![Downloads](https://poser.pugx.org/open-function-computers-llc/rad-theme-engine/d/total.svg)](https://packagist.org/packages/open-function-computers-llc/rad-theme-engine)<br>
📦 &nbsp;[View on Packagist](https://packagist.org/packages/open-function-computers-llc/rad-theme-engine) <br>
📃 &nbsp;[Read the Docs](https://rad-theme-engine.ofco.cloud/)
<br>
## About
The purpose of this package is to enable developers to use the familiar model-view-controller pattern in the creation of WordPress themes. This is accomplished by keeping HTML and PHP code as seperated as possible and adding convenient methods to organize data before its sent to the view controllers. Querying for posts, rendering menus, handling taxonomies and all the other essential parts of developing a WordPress theme are now easier than ever with the __RAD Theme Engine__.
## Quick Start
Inside of your site's `wp-content/themes` folder, run the following command to create a new __Rad Theme Engine__ project.
```
composer create-project open-function-computers-llc/wp-theme <theme-name>
```
Next, enter your new theme's folder and run `npm install` to get dependencies.
```
cd <theme-name>
npm install
```
And that's it! Read about advanced installations and asset bundling on [the docs](https://rad-theme-engine.ofco.cloud/).
## Example Projects
- [Shirt Store](https://github.com/open-function-computers-llc/rad-theme-engine-example-theme) Demonstrates custom post types, taxonomies, handlebars, and more.
## Authors
- Kurtis Holsapple [@lapubell](https://github.com/lapubell)
- Escher Wright-Dykhouse [@escherwd](https://github.com/escherwd)
- Gabriel Johnson - [@gabriel-johnson](https://github.com/gabriel-johnson)
## License
Licensed under the MIT license, see [LICENSE](https://github.com/open-function-computers-llc/rad-theme-engine/blob/main/LICENSE)

View File

@@ -0,0 +1,23 @@
{
"name": "open-function-computers-llc/rad-theme-engine",
"description": "A suite of classes to make WordPress theme development cleaner",
"require": {
"php": ">=7.4",
"jjgrainger/posttypes": "^2.1",
"salesforce/handlebars-php": "^2.3"
},
"require-dev": {
"phpunit/phpunit": "^9.5"
},
"license": "MIT",
"keywords": ["wordpress", "handlebars", "theme"],
"minimum-stability": "stable",
"autoload": {
"psr-4": {
"ofc\\": "src/"
}
},
"scripts": {
"post-create-project-cmd": "ofc\\RadThemeEngine::setup"
}
}

View File

@@ -0,0 +1,93 @@
<?php
return [
/**
* excerpt-length
* how many words should the wordpress excerpt be
*/
"excerpt-length" => 100,
/**
* guest-class
* if you want wordpress to automatically append a class to the body_class
* list when users are not authenticated, put that class name here. it
* defaults to "guest"
*
* to disable, set this to null
*/
"guest-class" => "null",
/**
* menu-locations
* register your individual menu locations here
*/
"menu-locations" => [
"main-nav" => "Main Navigation",
"footer-nav" => "Footer Navigation",
],
/**
* custom-post-types
* here is where you can define your custom post types easily
*
* icons are powered by dashicons, choose one from here:
* https://developer.wordpress.org/resource/dashicons
*
* additional options are enabled in the cpt options key
* if you override "supports", be sure to include 'title' and 'editor' in
* the list for standard wordpress functionality
*/
"custom-post-types" => [
[
"slug" => "thing",
"icon" => "dashicons-tide",
"options" => [
"supports" => ['title', 'editor', 'thumbnail', 'comments']
]
],
],
/**
* handlebars
*
* We use handlebars templating extensivly in this theme and code pattern.
* You can adjust the defaults for many attributes here.
*
* Set this to `false` to disable handlebars functionality completely
*/
"handlebars" => [
/**
* additional-helpers
* if you need to register additional Handlebars Helpers, register them here
*
* the key is the name that you will use in your templates, and the value is
* the callback function that is run on the template side
*/
"additional-helpers" => [],
/**
* template-extension
*
* The default extention for your templates is .tpl
* If you'd like to change that, set the vaule here, without the dot
*/
// "template-extension" => "tpl",
],
/**
* enable
* enable individual wordpress features here
*/
"enable" => [
"post-thumbnails",
"menus",
],
/**
* disable
* disable individual wordpress features here
*/
"disable" => [
"editor",
],
];

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
stopOnFailure="false"
>
<testsuites>
<testsuite name="Unit">
<directory suffix="Test.php">./tests</directory>
</testsuite>
</testsuites>
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">./src</directory>
</include>
</coverage>
</phpunit>

View File

@@ -0,0 +1,849 @@
<?php
namespace ofc;
//build our file, wysiwig, metaboxtoggler
//rename media to image
class FieldHTML
{
public static function template($type, $meta, $postID, $field)
{
$type = self::translateToSafeMethod($type);
return self::$type($meta, $postID, $field);
}
private static function translateToSafeMethod($type)
{
$output = str_replace(' ', '', ucwords(str_replace('-', ' ', $type)));
$output[0] = strtolower($output[0]);
return $output;
}
public static function wysiwyg($meta, $postID, $field)
{
//create wysiwyg editor
$settings = array(
'textarea_name' => 'post_text',
'default_editor' => 'TinyMce',
);
$wpEditor = wp_editor($meta, "rad_".$field['name']."_wysiwyg", $settings);
return <<<HTML
<div class="wysisyg">
<script>
label = '{{field.label}}'
wysName = '{{field.name}}'
value = "{{value}}"
meta = '{{ meta }}'
window.onload = function(){
//find elements of the editor
const wysiwygField = document.getElementById('rad_'+wysName+'_wysiwyg_ifr').contentWindow.document.getElementById('tinymce')
console.log(label)
const textareaField = document.getElementById('rad_'+wysName+'_wysiwyg')
const inputField = document.getElementById('rad_'+wysName)
// We have to use js to add the label for WYSIWYG field, otherwise it is under the field
var paragraph = document.createElement("p");
paragraph.className = "post-attributes-label-wrapper";
// Create a new label element
var labelElement = document.createElement("label");
labelElement.className = "post-attributes-label";
labelElement.htmlFor = "rad_"+name;
labelElement.textContent = label;
// Append the label to the paragraph
paragraph.appendChild(labelElement);
var targetElement = document.getElementById("wp-rad_"+wysName+"_wysiwyg-wrap");
targetElement.prepend(paragraph);
//on each keystroke update the input field
wysiwygField.addEventListener("input", function(){
content = tinymce.activeEditor.getContent({format: "html"})
inputField.value=content
})
textareaField.addEventListener("input", function(){
inputField.value=textareaField.value
})
}
</script>
<!-- input field that stores data -->
<input type="hidden" name="rad_{{ field.name }}" id="rad_{{ field.name }}" value="{{value}}"/>
</div>
HTML;
}
public static function text($meta, $postID, $field)
{
return <<<HTML
<p class="post-attributes-label-wrapper">
<label class="post-attributes-label" for="rad_{{ field.name }}">{{ field.label }}</label>
</p>
<input type="text" id="rad_{{ field.name }}" name="rad_{{ field.name }}" value="{{ value }}" />
HTML;
// {{ template }}
}
public static function number($meta, $postID, $field)
{
return <<<HTML
<p class="post-attributes-label-wrapper">
<label class="post-attributes-label" for="rad_{{ field.name }}">{{ field.label }}</label>
</p>
<input type="number" id="rad_{{ field.name }}" name="rad_{{ field.name }}" value="{{ value }}" />
HTML;
}
public static function textarea($meta, $postID, $field)
{
return <<<HTML
<p class="post-attributes-label-wrapper">
<label class="post-attributes-label" for="rad_{{ field.name }}">{{ field.label }}</label>
</p>
<textarea style="width: 100%; height: 220px;" id="rad_{{ field.name }}" name="rad_{{ field.name }}">{{ value }}</textarea>
HTML;
}
public static function repeater($meta, $postID, $field)
{
return <<<HTML
<p class="post-attributes-label-wrapper">
<label class="post-attributes-label" for="rad_{{ field.name }}">{{ field.label }}</label>
</p>
<div id="repeater-{{ field.name }}">
{{#raw}}
<input type="hidden" :name="id" :id="id" :value="valueString" />
<div v-if="!value">
<p>No elements</p>
</div>
<div v-else v-for="(row, i) in value" :key="'row-'+i">
<div>{{ i+1 }}</div>
<div v-for="(e, j) in row" :key="'e-'+j+'-row-'+i">
<label>{{ e.label }}</label>
<input v-if="e.type === 'text'" type="text" v-model="e.value" @change="updateValueString" />
<a href="#" v-if="e.type === 'media'" @click.prevent="setMediaFor(e)" class="button">Choose Media</a>
</div>
</div>
<div>
<a href="#" class="button" @click.prevent="addRow">Add Row</a>
</div>
{{/raw}}
</div>
<script>
if (!window.vm) {
var vm = [];
}
vm["{{ field.name }}"] = new Vue({
el: "#repeater-{{ field.name }}",
data: {
value: {{#json-encode value }},
valueString: "",
id: 'rad_{{ field.name }}',
newShape: {{#json-encode field.sub}},
fileFrame: null,
fileFrameTarget: null,
},
methods: {
addRow() {
var thingToPush = [];
for (let index = 0; index < this.newShape.length; index++) {
var e = this.newShape[index];
thingToPush.push(Object.assign({value: ""}, e));
}
this.value.push(thingToPush);
},
setMediaFor(ele) {
this.fileFrameTarget = ele;
this.fileFrame.open();
},
updateValueString: function() {
this.valueString = JSON.stringify(this.value);
},
},
mounted: function() {
if (!Array.isArray(this.value)) {
if (this.value.length < 1) {
this.value = [];
} else {
this.value = JSON.parse(this.value);
}
}
this.updateValueString();
var self = this;
jQuery(document).ready(function() {
self.fileFrame = wp.media({
frame: 'select',
state: 'mystate',
library: {type: 'image'},
multiple: false
});
self.fileFrame.states.add([
new wp.media.controller.Library({
id: 'mystate',
title: 'Choose Media',
priority: 20,
toolbar: 'select',
filterable: 'uploaded',
library: wp.media.query( self.fileFrame.options.library ),
multiple: false,
editable: true,
displayUserSettings: false,
displaySettings: false,
allowLocalEdits: true
})
]);
self.fileFrame.on('select', function() {
if (self.store === 'url') {
self.fileFrameTarget.value = self.fileFrame.state().get('selection').first().toJSON().url;
return;
}
// default storage is json
self.fileFrameTarget.value = self.fileFrame.state().get('selection').first().toJSON();
self.fileFrameTarget = null;
self.updateValueString();
});
});
}
});
</script>
HTML;
}
public static function image($meta, $postID, $field)
{
return <<<HTML
<p class="post-attributes-label-wrapper">
<label class="post-attributes-label" for="rad_{{ field.name }}">{{ field.label }}</label>
<div id="media-chooser-{{ field.name }}">
<script type="text/javascript">
//get all the variables associated with each image
meta = '{{ value }}'
postID = '{{ id }}';
name = "{{ field.name }}";
store = "{{ field.store }}";
//get container for image
container = document.getElementById('media-chooser-'+name);
//create image element to display currently set image if store type is url
if (store === 'url') {
function createIMG(metaPassed, namePassed, containerPassed){
image = document.createElement('img');
image.style.maxWidth = '300px';
image.src = metaPassed;
image.id=namePassed;
containerPassed.prepend(image);
}
if(meta){
createIMG(meta, name, container)
}
}
//create table element to display image and other associated information if store type is json
function createTableElements(div, table, textContent1, textContent2, idInput){
tr1 = document.createElement('tr');
td1 = document.createElement('td');
td1.style.fontWeight = 'bold';
td1.textContent = textContent1
td2 = document.createElement('td');
td2.textContent = textContent2;
td2.id=idInput
tr1.appendChild(td1)
tr1.appendChild(td2)
table.appendChild(tr1);
}
//not done, still need more values
if(store === 'json'){
//get the meta to readable json
//before this it is an ugly string, this makes js able to parse it
//create the image div to preview
function createTableIMG(url, name, postID, width, height, filesizeHumanReadable){
image = document.createElement('img');
image.style.maxWidth = '300px';
image.src = url;
image.id=name;
div = document.createElement('div')
table = document.createElement('table');
//create each table element
//each element displays either the post ID of the image, its URL, its dimmentisons, and its size
createTableElements(div, table, "ID: ", postID, name+'_id')
createTableElements(div, table, "URL: ", url, name+'_url')
//Probably need to change the full size at some point.
createTableElements(div, table, "Dimmensions: ",width+'x'+height, name+'_dim')
createTableElements(div, table, "Size: ", filesizeHumanReadable, name+'_size')
div.appendChild(table)
container.prepend(div)
container.prepend(image);
}
if(meta){
meta = meta.replace(/&quot;/g, '\\"');
meta = JSON.parse(meta)
createTableIMG(meta.url, name, postID, meta.sizes.full.width, meta.sizes.full.height, meta.filesizeHumanReadable)
}
}
</script>
<!-- Input field for each image -->
<input type="hidden" name="rad_{{ field.name }}" id="rad_{{ field.name }}" value="{{value}}"/>
<br />
<!-- The button to press to open modal -->
<!-- MUST PASS IN THE FIELD NAME AND STORE -->
<a href="#" onclick="chooseMedia('{{field.name}}', '{{field.store}}')" class="button">Choose Image</a>
</div>
</p>
<script>
// set up modal
function chooseMedia(fieldName, store) {
self.fileFrame = wp.media({
frame: 'select',
state: 'mystate',
library: { type: 'image' },
multiple: false
});
fileFrame.states.add([
new wp.media.controller.Library({
id: 'mystate',
title: fieldName,
priority: 20,
toolbar: 'select',
filterable: 'uploaded',
library: wp.media.query(fileFrame.options.library),
multiple: false,
editable: true,
displayUserSettings: false,
displaySettings: false,
allowLocalEdits: true
})
]);
//If image is selected update the input field value
fileFrame.on('select', function() {
inputField = document.getElementById('rad_'+fieldName)
attachment = fileFrame.state().get('selection').first().toJSON();
//check how we are to store the img
if(store == 'url')
{
//get image element
imgSrc = document.getElementById(fieldName)
//if it doesn't exist on the frontend make it and display so the user can see the image
if(imgSrc == null){
container = document.getElementById('media-chooser-'+fieldName)
createIMG(attachment.url, attachment.name, container)
}
//otherwise just updated the image
else{
imgSrc.src = attachment.url
}
//save it to the backend
inputField.value = attachment.url
} else //if store is json
{
//update the display immediatly
//Does NOT write anything to backend, only shows the user what it will look like.
imgSrc = document.getElementById(fieldName)
id = document.getElementById(fieldName+'_id')
if(id==null){
createTableIMG(attachment.url, attachment.name, attachment.id, attachment.sizes.full.width, attachment.sizes.full.height, attachment.filesizeHumanReadable)
}
else{
id = document.getElementById(fieldName+'_id')
url = document.getElementById(fieldName+'_url')
size = document.getElementById(fieldName+'_size')
dim = document.getElementById(fieldName+'_dim')
dim.textContent=attachment.sizes.full.width+'x'+attachment.sizes.full.height
size.textContent=attachment.filesizeHumanReadable
url.textContent=attachment.url
id.textContent=attachment.id
imgSrc.src = attachment.url
}
//this writes everything to backend
inputField.value = JSON.stringify(attachment)
}
});
//open the modal when button is pressed
fileFrame.open();
};
</script>
HTML;
}
public static function file($meta, $postID, $field)
{
return <<<HTML
<p class="post-attributes-label-wrapper">
<label class="post-attributes-label" for="rad_{{ field.name }}">{{ field.label }}</label>
<div id="media-chooser-{{ field.name }}">
<script type="text/javascript">
//get all the variables associated with each file
meta = '{{ value }}'
postID = '{{ id }}';
name = "{{ field.name }}";
//get container for file information
container = document.getElementById('media-chooser-'+name);
//function to create table elements for file info
function createTableElements(div, table, textContent1, textContent2, idInput){
tr1 = document.createElement('tr');
td1 = document.createElement('td');
td1.style.fontWeight = 'bold';
td1.textContent = textContent1
td2 = document.createElement('td');
td2.textContent = textContent2;
td2.id=idInput
tr1.appendChild(td1)
tr1.appendChild(td2)
table.appendChild(tr1);
}
//instantiate the table itself
function createTable(postID, url, title, filename, filesizeHumanReadable){
div = document.createElement('div')
table = document.createElement('table');
//create each table element
//each element displays either the post ID of the image, its URL, its dimmentisons, and its size
createTableElements(div, table, "URL: ", url, name+'_url')
//Probably need to change the full size at some point.
createTableElements(div, table, "Name: ", title, name+'_name')
createTableElements(div, table, "File Name: ", filename, name+'_fname')
createTableElements(div, table, "Size: ", filesizeHumanReadable, name+'_size')
div.appendChild(table)
container.appendChild(div)
}
//check if meta is already populated
if(meta){
//get the meta to readable json
//before this it is an ugly string, this makes js able to parse it
meta = meta.replace(/&quot;/g, '\\"');
meta = JSON.parse(meta)
createTable(postID, meta.url, meta.title, meta.filename, meta.filesizeHumanReadable)
}
</script>
<!-- Input field for each image -->
<input type="hidden" name="rad_{{ field.name }}" id="rad_{{ field.name }}" value="{{value}}"/>
<br />
<!-- The button to press to open modal -->
<!-- MUST PASS IN THE FIELD NAME AND STORE -->
<a href="#" onclick="chooseFile('{{field.name}}')" class="button">Choose File</a>
</div>
</p>
<script>
// set up modal
function chooseFile(fieldName) {
self.fileFrame = wp.media({
frame: 'select',
state: 'mystate',
library: { type: 'application' },
multiple: false
});
fileFrame.states.add([
new wp.media.controller.Library({
id: 'mystate',
title: fieldName,
priority: 20,
toolbar: 'select',
filterable: 'uploaded',
library: wp.media.query(fileFrame.options.library),
multiple: false,
editable: true,
displayUserSettings: false,
displaySettings: false,
allowLocalEdits: true
})
]);
//If file is selected update the input field value
fileFrame.on('select', function() {
inputField = document.getElementById('rad_'+fieldName)
attachment = fileFrame.state().get('selection').first().toJSON();
fileName = document.getElementById(fieldName+'_fname')
//if there isn't already the filename element
//(i.e. this is the first time uploading a file and we need to create the table to display the data)
//create the table and populate it with the recently uploaded information
if(fileName == null){
console.log("null")
createTable(attachment.postID, attachment.url, attachment.title, attachment.filename, attachment.filesizeHumanReadable)
}
//otherwise we can update the existing info
else{
fileName = document.getElementById(fieldName+'_fname')
url = document.getElementById(fieldName+'_url')
newName = document.getElementById(fieldName+'_name')
size = document.getElementById(fieldName+'_size')
size.textContent=attachment.filesizeHumanReadable
newName.textContent=attachment.title
url.textContent=attachment.url
fileName.textContent = attachment.filename
}
//this writes everything to backend
inputField.value = JSON.stringify(attachment)
});
//open the modal when button is pressed
fileFrame.open();
};
</script>
HTML;
}
public static function flexRepeater()
{
return <<<HTML
<p class="post-attributes-label-wrapper">
<label class="post-attributes-label" for="rad_{{ field.name }}">{{ field.label }}</label>
<div id="media-chooser-{{ field.name }}">
{{#raw}}
<input type="hidden" :name="id" :id="id" :value="valueString" />
<div v-if="!value">
<p>You don't have any content variations. Start adding some with the "Add Variation" button below.</p>
</div>
<div v-else>
<div v-for="(v, i) in value" :key="'variation-'+i">
<div class="variation-wrapper">
<div class="title">
<h3>{{ v.name }}</h3>
</div>
<div class="controls">
<a v-if="i > 0" class="button button-small" style="font-family: dashicons" href="#" @click.prevent="moveUp(i)"></a>
<a v-if="i < (value.length - 1)" class="button button-small" style="font-family: dashicons" href="#" @click.prevent="moveDown(i)"></a>
<a class="button button-small" style="font-family: dashicons" href="#" @click.prevent="removeVariation(i)"></a>
</div>
<div class="content">
<div v-for="(f, j) in v.fields" :key="'variation-'+i+'-field-'+j" />
<label>{{ f.label }}</label>
<!-- simple fields -->
<input type="text" v-if="f.type === 'text'" v-model="f.value" @change="updateValueString" />
<textarea v-if="f.type === 'textarea'" v-model="f.value" style="width: 100%; height: 220px;" @change="updateValueString"></textarea>
<!-- ajax fields -->
<div v-if="f.type === 'media'" style="position: relative">
<table v-if="chosenMediaParse(f.value)">
<tr>
<td style="font-weight: bold">ID:</td><td>{{ chosenMediaParse(f.value).id }}</td>
</tr>
<td style="font-weight: bold">Size:</td><td>{{ chosenMediaParse(f.value).filesizeHumanReadable }}</td>
</tr>
<td style="font-weight: bold">Dimensions:</td><td>{{ chosenMediaParse(f.value).width }}x{{ chosenMediaParse(f.value).height }}</td>
</tr>
<td style="font-weight: bold">URL:</td><td>{{ chosenMediaParse(f.value).url }}</td>
</tr>
</table>
<div v-if="!f.value" style="border: 1px solid #666; padding: 1rem; margin-bottom: 1rem;">No media chosen</div>
<a href="#" @click.prevent="chooseMedia(f)" class="button">Choose Media</a>
</div>
<!-- ajax fields -->
<div v-if="f.type === 'related'" style="position: relative">
<input type="text" @keyup="relatedTypeAhead(\$event)" v-model="ajaxSearches[f.name]" placeholder="Type to choose..." />
<ul class="pop-open-box">
<li v-for="(r, k) in ajaxResults" :key="'ajax-result-'+k" @click="setValueFor(r, f)">{{ r.title }}</li>
</ul>
<p v-if="f.value">{{ f.value.url }}</p>
</div>
</div>
<hr />
</div>
</div>
</div>
</div>
<div v-if="addingVariation">
<p>Type:</p>
<select v-model="newVariation">
<option v-for="(v, i) in variations" :value="v" :key="'variation-'+i">{{ v.name }}</option>
</select>
<a href="#" @click.prevent="addVariation" class="button button-small">Add</a>
<a href="#" @click.prevent="addingVariation = false; newVariation = null;" class="button button-small">Cancel</a>
</div>
<a
v-else
class="button button-small"
href="#"
@click.prevent="addingVariation = true"
>Add Variation</a>
{{/raw}}
</div>
<style>
.pop-open-box {
position: absolute;
top: 100%;
left: 0;
background-color: white;
padding: 3px;
width: 100%;
margin: 0;
}
.pop-open-box li {
padding: 3px;
}
.pop-open-box li:hover {
cursor: pointer;
background-color: grey;
}
.variation-wrapper {
display: flex;
flex-wrap: wrap;
}
.variation-wrapper h3 {
margin: 0;
}
.variation-wrapper .title {
flex: 1;
}
.variation-wrapper .content {
width: 100%;
}
.variation-wrapper label {
display: block;
font-weight: bold;
margin-top: 10px;
}
</style>
<script>
if (!window.vm) {
var vm = [];
}
vm["{{ field.name }}"] = new Vue({
el: "#media-chooser-{{ field.name }}",
data: {
id: 'rad_{{ field.name }}',
value: {{#json-encode value}},
variations: {{#json-encode field.variations}},
addingVariation: false,
newVariation: null,
valueString: "",
ajaxResults: [],
ajaxSearches: {},
fileFrame: null,
fileFrameTarget: null,
},
methods: {
addVariation: function(v) {
this.value.push(Object.assign({value: ""}, this.newVariation));
this.newVariation = null;
this.addingVariation = false;
},
moveUp: function(index) {
this.value.splice(index-1, 0, this.value.splice(index, 1)[0]);
},
moveDown: function(index) {
this.value.splice(index+1, 0, this.value.splice(index, 1)[0]);
},
removeVariation: function(index) {
this.value.splice(index, 1);
},
relatedTypeAhead: function(\$event) {
var self = this;
jQuery.post(ajaxurl, {action: "betterwordpress_related", q: \$event.target.value}, function(res) {
self.ajaxResults = JSON.parse(res);
});
},
setValueFor: function(r, f) {
f.value = r;
this.updateValueString();
this.ajaxResults = [];
this.ajaxSearches = {};
},
updateValueString: function() {
this.valueString = JSON.stringify(this.value);
},
chooseMedia: function(f) {
this.fileFrameTarget = f;
this.fileFrame.open();
},
chosenMediaParse(data) {
if (!data) {
return;
}
// console.log(data);
return JSON.parse(data);
}
},
watch: {
value: function() {
this.valueString = JSON.stringify(this.value);
}
},
mounted: function() {
if (!Array.isArray(this.value)) {
if (this.value.length < 1) {
this.value = [];
} else {
this.value = JSON.parse(this.value);
}
}
var self = this;
jQuery(document).ready(function() {
self.fileFrame = wp.media({
frame: 'select',
state: 'mystate',
library: {type: 'image'},
multiple: false
});
self.fileFrame.states.add([
new wp.media.controller.Library({
id: 'mystate',
title: 'Choose your media',
priority: 20,
toolbar: 'select',
filterable: 'uploaded',
library: wp.media.query( self.fileFrame.options.library ),
multiple: false,
editable: true,
displayUserSettings: false,
displaySettings: false,
allowLocalEdits: true
})
]);
self.fileFrame.on('select', function() {
if (self.store === 'url') {
self.fileFrameTarget.value = self.fileFrame.state().get('selection').first().toJSON().url;
self.updateValueString();
return;
}
// default storage is json
self.fileFrameTarget.value = JSON.stringify(self.fileFrame.state().get('selection').first().toJSON());
self.updateValueString();
});
});
}
});
</script>
HTML;
}
public static function metaBoxToggler($hidden)
{
// die(var_dump($hidden));
return <<<HTML
<style>#{{group-name}} {display: none;}</style>
<div id="rad-metabox-toggler-{{group-name}}"></div>
<script>
const elementsToHide = {{#json-encode hidden}}
document.getElementById("metaBoxToggler{{group-name}}")
function hideStuff(state, elementsToHide) {
// Store references to DOM elements
for (var i = 0; i < elementsToHide.length; i++) {
var element = elementsToHide[i];
if (element === "WYSIWYG") {
jQuery("#postdivrich").css("visibility", "hidden");
jQuery("#postdivrich").height(0);
}
}
}
jQuery(document).ready(function() {
jQuery("#{{group-name}}").show();
jQuery("#{{group-name}} input, #{{group-name}} textarea").each(function() {
jQuery(this).removeAttr("disabled");
});
hideStuff(true, elementsToHide);
return;
jQuery("#{{group-name}} input, #{{group-name}} textarea").each(function() {
jQuery(this).attr("disabled", "disabled");
});
jQuery("#{{group-name}}").hide();
// bind event to this vue app
jQuery("#page_template").on("change", function() {
self.selectedTemplate = jQuery("#page_template option:selected").text();
});
// initial state hydration
self.selectedTemplate = jQuery("#page_template option:selected").text();
// hideStuff(elementsToHide)
});
// Initialize the module
</script>
HTML;
}
}

View File

@@ -0,0 +1,84 @@
<?php
namespace ofc;
class RadField
{
public static function image($label, $name = null, $store = "url") : array
{
if (is_null($name)) {
$name = Util::snakeify($label);
}
return [
"type" => "image",
"label" => $label,
"name" => $name,
"store" => $store,
];
}
public static function text($label, $name = null) : array
{
if (is_null($name)) {
$name = Util::snakeify($label);
}
return [
"type" => "text",
"label" => $label,
"name" => $name,
];
}
public static function textarea($label, $name = null) : array
{
if (is_null($name)) {
$name = Util::snakeify($label);
}
return [
"type" => "textarea",
"label" => $label,
"name" => $name,
];
}
public static function file($label, $name = null) : array
{
if (is_null($name)) {
$name = Util::snakeify($label);
}
return [
"type" => "file",
"label" => $label,
"name" => $name,
];
}
public static function wysiwyg($label, $name = null) : array
{
if (is_null($name)) {
$name = Util::snakeify($label);
}
return [
"type" => "wysiwyg",
"label" => $label,
"name" => $name,
];
}
public static function getFields($fields): array
{
$tpl_fields = [];
foreach($fields as $field){
$tpl_fields[] = 'rad.'.$field['name'];
}
return $tpl_fields;
}
}

View File

@@ -0,0 +1,85 @@
<?php
namespace ofc;
class RadThemeEngine
{
public static function wpHeader()
{
return function ($template, $context, $args, $source) {
return self::getFromBuffer("wp_head");
};
}
public static function wpTitle()
{
return function ($template, $context, $args, $source) {
return wp_title('|', false, 'right') . get_bloginfo("name");
};
}
public static function wpFooter()
{
return function ($template, $context, $args, $source) {
return self::getFromBuffer("wp_footer");
};
}
public static function bodyClasses()
{
return function ($template, $context, $args, $source) {
return self::getFromBuffer("body_class");
};
}
public static function jsonEncode()
{
return function ($template, $context, $args, $source) {
return json_encode($context->get($args));
};
}
public static function jsonAccess()
{
return function ($template, $context, $args, $source) {
$parts = explode(".", $args);
if (count($parts) != 2) {
return "Invalid use of json-access";
}
$data = json_decode($context->get($parts[0]), true);
return $data[$parts[1]];
};
}
public static function processFlex()
{
return function ($template, $context, $args, $source) {
$output = "";
die(var_dump($context->get($args)));
$groups = json_decode($context->get($args));
foreach ($groups as $g) {
$data = [];
foreach ($g->fields as $f) {
$data[$f->name] = $f->value;
}
$output .= site()->render($g->tpl, $data);
}
return $output;
};
}
public static function nl2br()
{
return function ($template, $context, $args, $source) {
return nl2br($context->get($args));
};
}
private static function getFromBuffer($func)
{
ob_start();
$func();
$output = ob_get_clean();
return $output;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,97 @@
<?php
namespace ofc;
class Util
{
/**
* slugify
* modified from https://lucidar.me/en/web-dev/how-to-slugify-a-string-in-php/
*
* @param string $str
* @return string
*/
public static function slugify(string $str) : string
{
$text = strip_tags($str);
$text = str_replace(" & ", " and ", $text);
$text = preg_replace('~[^\pL\d]+~u', '-', $text);
setlocale(LC_ALL, 'en_US.utf8');
$text = iconv('utf-8', 'us-ascii//TRANSLIT', $text);
$text = preg_replace('~[^-\w]+~', '', $text);
$text = trim(strtolower($text), '-');
$text = preg_replace('~-+~', '-', $text);
if (empty($text)) {
return 'n-a';
}
return $text;
}
/**
* snakify
* basically the same as slugify but with _ instead of -
*
* @param string $string
* @return string
*/
public static function snakeify(string $string) :string
{
return str_replace("-", "_", self::slugify($string));
}
public static function processFieldGroup($fieldGroup)
{
add_action('admin_head-post.php', function () use ($fieldGroup) {
global $post;
if ($fieldGroup) {
echo site()->renderTemplate(FieldHTML::metaBoxToggler(["WYSIWYG"]), [
"group-name" => self::slugify($fieldGroup[0]),
"for" => "page",
"hidden" => ["WYSIWYG"],
]);
}
});
add_action('add_meta_boxes', function () use ($fieldGroup) {
$name = $fieldGroup[0];
array_shift($fieldGroup);
foreach ($fieldGroup as $group) {
// conditionally show/hide the box
$slugName = self::slugify($name);
add_meta_box(
$slugName,
$name,
function ($post) use ($fieldGroup, $group) {
foreach ($fieldGroup as $field) {
// sanatize media fields
if ($field["type"] === "image") {
if (!isset($field["store"])) {
$field["store"] = "json";
}
}
echo site()->renderTemplate(FieldHTML::template($field["type"], get_post_meta($post->ID, "rad_".$field['name'], true), $post->ID, $field), [
"value" => get_post_meta($post->ID, "rad_".$field['name'], true),
"id" => $post->ID,
"field" => $field,
])."<hr />";
}
},
'page',
'advanced',
);
}
});
add_action('save_post', function ($post_id) use ($fieldGroup) {
foreach ($fieldGroup['fields'] as $field) {
if (!isset($_POST['rad_'.$field['name']])) {
continue;
}
update_post_meta($post_id, 'rad_'.$field['name'], wp_kses_post($_POST['rad_'.$field['name']]));
}
});
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace ofc\tests;
use ofc\Util;
use PHPUnit\Framework\TestCase;
class UtilTest extends TestCase
{
/** @test */
public function weCanSlugifyAString()
{
$cases = [
"This is a string" => "this-is-a-string",
"String with nÓn ASCII chars & stuff!" => "string-with-non-ascii-chars-and-stuff",
];
foreach ($cases as $input => $expected) {
$this->assertEquals($expected, Util::slugify($input));
}
}
}