diff --git a/src/actions/CompileTwigComponent.php b/src/actions/CompileTwigComponent.php index 93ec6b5..baa71ee 100644 --- a/src/actions/CompileTwigComponent.php +++ b/src/actions/CompileTwigComponent.php @@ -5,10 +5,10 @@ use Craft; use craft\helpers\App; use craft\web\View; -use markhuot\keystone\base\ComponentData; use markhuot\keystone\base\ComponentType; use markhuot\keystone\base\FieldDefinition; use markhuot\keystone\models\Component; +use markhuot\keystone\models\ComponentData; use PhpParser\Builder\Class_; use PhpParser\Node; use PhpParser\Node\Stmt; diff --git a/src/base/ComponentData.php b/src/base/ComponentData.php deleted file mode 100644 index 09cdd12..0000000 --- a/src/base/ComponentData.php +++ /dev/null @@ -1,62 +0,0 @@ -data; - } - - public function getAccessed(): Collection - { - return collect($this->accessed); - } - - public function offsetExists(mixed $offset): bool - { - return true; - } - - public function offsetGet(mixed $offset): mixed - { - if (is_string($offset)) { - $this->accessed[$offset] = $this->accessed[$offset] ?? new FieldDefinition(); - } - - $value = $this->data[$offset] ?? null; - - if ($this->normalize) { - return ($this->normalize)($offset, $value); - } - - return $value; - } - - public function offsetSet(mixed $offset, mixed $value): void - { - $this->data[$offset] = $value; - } - - public function offsetUnset(mixed $offset): void - { - unset($this->data[$offset]); - } - - public function defineField(string $handle): FieldDefinition - { - return $this->accessed[$handle] = $this->accessed[$handle] ?? new FieldDefinition(); - } -} diff --git a/src/controllers/ComponentsController.php b/src/controllers/ComponentsController.php index 62981e7..b761386 100644 --- a/src/controllers/ComponentsController.php +++ b/src/controllers/ComponentsController.php @@ -7,6 +7,8 @@ use markhuot\keystone\actions\AddComponent; use markhuot\keystone\actions\GetComponentType; use markhuot\keystone\models\Component; +use markhuot\keystone\models\ComponentData; +use markhuot\keystone\models\ComponentElement; class ComponentsController extends Controller { @@ -33,25 +35,36 @@ public function actionAdd() public function actionStore() { + $componentData = new ComponentData; + $componentData->type = $this->request->getRequiredBodyParam('type'); + $componentData->save(); + $component = new Component; $component->elementId = $elementId = $this->request->getRequiredBodyParam('elementId'); $component->fieldId = $fieldId = $this->request->getRequiredBodyParam('fieldId'); + $component->dataId = $componentData->id; $component->path = $path = $this->request->getBodyParam('path'); - $slot = $this->request->getBodyParam('slot'); - $slot = $slot === '' ? null : $slot; - $component->slot = $slot; + $component->slot = $this->request->getBodyParam('slot'); $component->type = $this->request->getRequiredBodyParam('type'); $component->sortOrder = ((Component::find()->where([ 'elementId' => $elementId, 'fieldId' => $fieldId, - 'path' => $path, - 'slot' => $slot, + 'path' => $component->path, + 'slot' => $component->slot, ])->max('sortOrder')) ?? -1) + 1; $component->save(); + $element = Craft::$app->elements->getElementById($component->elementId); + $field = Craft::$app->fields->getFieldById($component->fieldId); + return $component->errors ? $this->asFailure('Oh no') : - $this->asSuccess('Component added'); + $this->asSuccess('Component added', [ + 'elementId' => $component->elementId, + 'fieldId' => $component->fieldId, + 'fieldHandle' => $field->handle, + 'fieldHtml' => $field->getInputHtml(null, $element), + ]); } public function actionEdit() @@ -76,10 +89,18 @@ public function actionUpdate() $data = $this->request->getBodyParam('fields', []); $component = Component::findOne(['id' => $id]); - $component->data = array_merge($component->data->toArray(), $data); - $component->save(); + $component->data->merge($data); + $component->data->save(); + + $element = Craft::$app->elements->getElementById($component->elementId); + $field = Craft::$app->fields->getFieldById($component->fieldId); - return $this->asSuccess('Component saved'); + return $this->asSuccess('Component saved', [ + 'elementId' => $component->elementId, + 'fieldId' => $component->fieldId, + 'fieldHandle' => $field->handle, + 'fieldHtml' => $field->getInputHtml(null, $element), + ]); } public function actionGetEditModalHtml() diff --git a/src/db/ActiveRecord.php b/src/db/ActiveRecord.php index 873d4f8..349541c 100644 --- a/src/db/ActiveRecord.php +++ b/src/db/ActiveRecord.php @@ -9,10 +9,11 @@ class ActiveRecord extends \craft\db\ActiveRecord public function __set($name, $value): void { if (method_exists($this, $methodName= 'set' . ucfirst($name))) { - $value = $this->{$methodName}($value); + $this->{$methodName}($value); + } + else { + parent::__set($name, $value); } - - parent::__set($name, $value); } public function getRawAttributes(): array diff --git a/src/db/Table.php b/src/db/Table.php index d2fe022..ee2671d 100644 --- a/src/db/Table.php +++ b/src/db/Table.php @@ -5,5 +5,5 @@ class Table { const COMPONENTS = '{{%keystone_components}}'; - const COMPONENTS_ELEMENTS = '{{%keystone_components_elements}}'; + const COMPONENT_DATA = '{{%keystone_component_data}}'; } diff --git a/src/fields/Keystone.php b/src/fields/Keystone.php index 4d16540..9f2dbd9 100644 --- a/src/fields/Keystone.php +++ b/src/fields/Keystone.php @@ -8,6 +8,7 @@ use craft\web\View; use markhuot\keystone\actions\GetComponentType; use markhuot\keystone\models\Component; +use markhuot\keystone\models\ComponentElement; use Twig\Markup; class Keystone extends Field @@ -15,11 +16,6 @@ class Keystone extends Field protected function getFragment(ElementInterface $element) { $component = new Component; - $component->elementId = $element->id; - $component->fieldId = $this->id; - $component->sortOrder = 0; - $component->level = 0; - $component->slot = null; $component->type = 'keystone/fragment'; $component->setSlotted(Component::find()->where([ 'elementId' => $element->id, diff --git a/src/migrations/Install.php b/src/migrations/Install.php index eeedab0..ca8ab13 100644 --- a/src/migrations/Install.php +++ b/src/migrations/Install.php @@ -9,26 +9,24 @@ class Install extends Migration { public function safeUp() { - $this->createTable(Table::COMPONENTS, [ + $this->createTable(Table::COMPONENT_DATA, [ 'id' => $this->primaryKey(), - 'elementId' => $this->bigInteger()->unsigned(), - 'fieldId' => $this->bigInteger()->unsigned(), 'type' => $this->string(256), - 'sortOrder' => $this->integer(), - 'path' => $this->string(1024), - 'level' => $this->integer(), - 'slot' => $this->string(256), 'data' => $this->json(), 'dateCreated' => $this->dateTime()->notNull(), 'dateUpdated' => $this->dateTime()->notNull(), 'uid' => $this->uid(), ]); - $this->createTable(Table::COMPONENTS_ELEMENTS, [ + $this->createTable(Table::COMPONENTS, [ 'id' => $this->primaryKey(), 'elementId' => $this->bigInteger()->unsigned(), 'fieldId' => $this->bigInteger()->unsigned(), - 'componentId' => $this->bigInteger()->unsigned(), + 'dataId' => $this->bigInteger()->unsigned(), + 'sortOrder' => $this->integer(), + 'path' => $this->string(1024), + 'level' => $this->integer(), + 'slot' => $this->string(256), 'dateCreated' => $this->dateTime()->notNull(), 'dateUpdated' => $this->dateTime()->notNull(), 'uid' => $this->uid(), @@ -40,7 +38,7 @@ public function safeUp() public function safeDown() { $this->dropTableIfExists(Table::COMPONENTS); - $this->dropTableIfExists(Table::COMPONENTS_ELEMENTS); + $this->dropTableIfExists(Table::COMPONENT_DATA); return true; } diff --git a/src/models/Component.php b/src/models/Component.php index c8354ac..ddbab42 100644 --- a/src/models/Component.php +++ b/src/models/Component.php @@ -3,7 +3,6 @@ namespace markhuot\keystone\models; use markhuot\keystone\actions\GetComponentType; -use markhuot\keystone\base\ComponentData; use markhuot\keystone\base\ComponentType; use markhuot\keystone\collections\SlotCollection; use markhuot\keystone\db\ActiveRecord; @@ -12,25 +11,43 @@ class Component extends ActiveRecord { - protected ComponentData $_data; - protected ?array $slotted = null; protected array $accessed = []; + protected ?array $slotted = null; public static function factory() { return new \markhuot\keystone\factories\Component; } - public function safeAttributes() + public function getData() { - return array_merge(parent::safeAttributes(), ['path', 'data']); + return $this->hasOne(ComponentData::class, ['id' => 'dataId']); } - public function rules(): array + public function __get($name) { - return [ - [['elementId', 'fieldId', 'sortOrder', 'slot', 'type'], 'required'], - ]; + $value = parent::__get($name); + + if ($name === 'data' && $value === null) { + $this->populateRelation($name, $data=new ComponentData); + $value = $data; + } + + if ($name === 'data') { + $value->setNormalizer(fn ($handle, $value) => $this->getType()->getField($handle)->normalizeValue($value)); + } + + return $value; + } + + public function setType($type) + { + $this->data->setAttribute('type', $type); + } + + public function getType(): ComponentType + { + return (new GetComponentType)->byType($this->data->type); } public static function tableName() @@ -38,34 +55,42 @@ public static function tableName() return Table::COMPONENTS; } - public function setPath(string $path): ?string + public function setSlotted(array $components) { - $path = trim($path, '/'); - - if (empty($path)) { - $path = null; - } + $this->slotted = $components; + } - return $path; + public function getAccessed() + { + return $this->accessed; } - public function getChildPath(): ?string + public function safeAttributes() { - $path = implode('/', array_filter([$this->path, $this->id])); + return array_merge(parent::safeAttributes(), ['path', 'slot']); + } - return ($path !== '') ? $path : null; + public function rules(): array + { + return [ + [['elementId', 'fieldId', 'dataId', 'sortOrder'], 'required'], + ]; } - protected function prepareForDb(): void + public function setPath(string $path): void { - parent::prepareForDb(); + $path = trim($path, '/'); - $this->level = count(array_filter(explode('/', $this->path))); + if (empty($path)) { + $path = null; + } + + $this->setAttribute('path', $path); } - public function getType(): ComponentType + public function setSlot(?string $slot): void { - return (new GetComponentType)->byType($this->type); + $this->setAttribute('slot', $slot === '' ? null : $slot); } public function render() @@ -76,16 +101,6 @@ public function render() ]), 'utf-8'); } - public function setSlotted(array $components) - { - $this->slotted = $components; - } - - public function getAccessed() - { - return $this->accessed; - } - public function getSlot($name=null) { $this->accessed[] = $name; @@ -123,36 +138,17 @@ public function getSlot($name=null) return new SlotCollection($this, $name, $components); } - public function newData(): ComponentData + public function getChildPath(): ?string { - return new ComponentData( - data: $this->getRawAttributes()['data'] ?? [], - normalize: fn ($key, $value) => $this->getType()->getField($key)?->normalizeValue($value) ?? $value, - ); + $path = implode('/', array_filter([$this->path, $this->id])); + + return ($path !== '') ? $path : null; } - /** - * We override the `slot` attribute for a method call to ->getSlot() and then expose - * the raw ->attributes['slot'] as ->__get('slotName') - */ - public function __get($name) + protected function prepareForDb(): void { - if ($name === 'slot') { - return $this->getSlot()->toHtml(); - } - - if ($name === 'slotName') { - return $this->attributes['slot'] ?? null; - } - - if ($name === 'data') { - if (isset($this->_data)) { - return $this->_data; - } - - return $this->_data = $this->newData(); - } + parent::prepareForDb(); - return parent::__get($name); + $this->level = count(array_filter(explode('/', $this->path))); } } diff --git a/src/models/ComponentData.php b/src/models/ComponentData.php new file mode 100644 index 0000000..60fcbd1 --- /dev/null +++ b/src/models/ComponentData.php @@ -0,0 +1,82 @@ +normalizer = $normalizer; + + return $this; + } + + public function getAccessed() + { + return collect($this->accessed); + } + + public function offsetExists(mixed $offset): bool + { + return true; + } + + public function offsetGet(mixed $offset): mixed + { + $this->accessed[$offset] = new FieldDefinition; + + $value = $this->getAttribute('data')[$offset] ?? null; + + if ($this->normalizer) { + return ($this->normalizer)($offset, $value); + } + + return $value; + } + + public function offsetSet(mixed $offset, mixed $value): void + { + $this->merge([$offset => $value]); + } + + public function offsetUnset(mixed $offset): void + { + $old = $this->getAttribute('data') ?? []; + unset($old[$offset]); + $this->setAttribute('data', $old); + } + + public function merge(array $new): void + { + $old = $this->getAttribute('data') ?? []; + $this->setAttribute('data', array_merge($old, $new)); + } +} diff --git a/src/resources/keystone.js b/src/resources/keystone.js index 16d5653..7ff4bfa 100644 --- a/src/resources/keystone.js +++ b/src/resources/keystone.js @@ -1,16 +1,17 @@ document.addEventListener('click', event => { - if (typeof event.target.dataset.openKeystoneComponentEditor === 'undefined') { - return; - } + if (typeof event.target.dataset.openKeystoneComponentEditor === 'undefined') { + return; + } - event.preventDefault(); - event.stopPropagation(); - const id = event.target.dataset.keystoneComponentId; + event.preventDefault(); + event.stopPropagation(); + const id = event.target.dataset.keystoneComponentId; const slideout = new Craft.CpScreenSlideout('keystone/components/edit', {params: {id}}); slideout.on('submit', ev => { - Craft.cp.$primaryForm.append(Object.assign(document.createElement('input'), {name: 'fields[myKeystoneField]', value: new Date().getTime()})) + //Craft.cp.$primaryForm.append(Object.assign(document.createElement('input'), {name: 'fields[myKeystoneField]', value: new Date().getTime()})) + Craft.cp.$primaryForm.get(0).querySelector('[data-attribute="myKeystoneField"] .input').innerHTML = ev.response.data.fieldHtml }); slideout.on('close', () => { @@ -30,7 +31,7 @@ document.addEventListener('click', event => { const slideout = new Craft.CpScreenSlideout('keystone/components/add', {params}); slideout.on('submit', ev => { - // ev.data ... + Craft.cp.$primaryForm.get(0).querySelector('[data-attribute="myKeystoneField"] .input').innerHTML = ev.response.data.fieldHtml }); slideout.on('close', () => { diff --git a/src/templates/components/text.twig b/src/templates/components/text.twig index fd51ee2..e1829d6 100644 --- a/src/templates/components/text.twig +++ b/src/templates/components/text.twig @@ -1,4 +1,6 @@ -{% do props.defineField('text').multiline(true) %} +{% export propTypes = { + text: field('plaintext').multiline(true) +} %} {% export icon %}{% endexport %}