Skip to content
1 change: 1 addition & 0 deletions packages/cms/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const {
requireElevatedSession,
requireElevatedSessionIf,
clone,
debounce,
deepClone,
resetValuesFromResponse,
} = __STATAMIC__.core;
1 change: 1 addition & 0 deletions resources/js/bootstrap/cms/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ export { default as HasActionsMixin } from '../../components/publish/HasActions.
export { default as resetValuesFromResponse } from '../../util/resetValuesFromResponse.js';
export { requireElevatedSession, requireElevatedSessionIf } from '../../components/elevated-sessions';
export { default as clone, deepClone } from '../../util/clone.js';
export { default as debounce } from '../../util/debounce.js';
2 changes: 1 addition & 1 deletion resources/js/components/ui/Publish/Components.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ const { components } = injectContainerContext();
</script>

<template>
<component v-for="component in components" :key="component.name" :is="component.name" v-bind="component.props" />
<component v-for="component in components" :key="component.name" :is="component.name" v-bind="component.props" v-on="component.events" />
</template>
58 changes: 57 additions & 1 deletion resources/js/components/ui/Publish/Container.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const [injectContainerContext, provideContainerContext, containerContextK

<script setup>
import { nanoid as uniqid } from 'nanoid';
import { watch, ref, computed, toRef, nextTick } from 'vue';
import { onMounted, onUnmounted, watch, ref, computed, toRef, nextTick } from 'vue';
import Component from '@/components/Component.js';
import Tabs from './Tabs.vue';
import Values from '@/components/publish/Values.js';
Expand Down Expand Up @@ -210,6 +210,10 @@ function setFieldValue(path, value) {
data_set(values.value, path, value);
}

function setMeta(newMeta) {
meta.value = newMeta;
}

function setFieldMeta(path, value) {
data_set(meta.value, path, value);
}
Expand All @@ -236,6 +240,30 @@ function removeLocalizedField(path) {
if (index !== -1) localizedFields.value.splice(index, 1);
}

const fieldFocus = ref({});

const fieldLocks = computed(() => {
const locks = {};
for (const { handle, user } of Object.values(fieldFocus.value)) {
if (!locks[handle]) {
locks[handle] = user;
}
}
return locks;
});

function focusField(handle, user = Statamic.user) {
if (handle.includes('.')) throw new Error('focusField only supports top-level fields.');
fieldFocus.value[user.id] = { handle, user };
}

function blurField(handle, user = Statamic.user) {
if (handle.includes('.')) throw new Error('blurField only supports top-level fields.');
if (fieldFocus.value[user.id]?.handle === handle) {
delete fieldFocus.value[user.id];
}
}

function pushComponent(name, { props }) {
const component = new Component(uniqid(), name, props);
components.value.push(component);
Expand Down Expand Up @@ -273,11 +301,16 @@ const builtInProvides = {
isTrackingOriginValues: computed(() => !!props.originValues),
setValues,
setFieldValue,
setMeta,
setFieldMeta,
setFieldPreviewValue,
setRevealerField,
unsetRevealerField,
setHiddenField,
fieldFocus,
fieldLocks,
focusField,
blurField,
isDirty,
withoutDirtying,
};
Expand All @@ -294,6 +327,28 @@ const provided = { ...additionalProvides, ...builtInProvides };

provideContainerContext({ ...provided, container: provided });

onMounted(() => {
Statamic.$events.$emit('publish-container-created', {
name: props.name,
reference: props.reference,
site: props.site,
values,
setFieldValue,
setValues,
meta,
setMeta,
setFieldMeta,
pushComponent,
fieldFocus,
focusField,
blurField,
});
});

onUnmounted(() => {
Statamic.$events.$emit('publish-container-destroyed', { name: props.name });
});

defineExpose({
name: props.name,
values,
Expand All @@ -306,6 +361,7 @@ defineExpose({
pushComponent,
visibleValues,
setValues,
setMeta,
setExtraValues,
});

Expand Down
18 changes: 13 additions & 5 deletions resources/js/components/ui/Publish/Field.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ const {
setFieldMeta,
hiddenFields,
setHiddenField,
fieldLocks,
focusField,
blurField,
container,
direction,
} = injectContainerContext();
Expand Down Expand Up @@ -112,11 +115,14 @@ watch(
);

function focused() {
// todo
if (fieldPathPrefix.value) return;
focusField(handle);
}

function blurred() {
// todo
function blurred(event) {
if (fieldPathPrefix.value) return;
if (event?.currentTarget?.contains(event.relatedTarget)) return;
blurField(handle);
}

const values = computed(() => {
Expand Down Expand Up @@ -170,7 +176,8 @@ const isReadOnly = computed(() => {
return isLocked.value || props.config.visibility === 'read_only' || false;
});

const isLocked = computed(() => false); // todo
const lockedBy = computed(() => fieldLocks.value[handle] ?? null);
const isLocked = computed(() => lockedBy.value !== null && lockedBy.value.id !== Statamic.user.id);

const isSyncable = computed(() => {
// Only top-level fields can be synced.
Expand Down Expand Up @@ -244,6 +251,7 @@ const fieldtypeComponentEvents = computed(() => ({
<template v-else-if="config.hide_display">
<span class="sr-only">{{ __(config.display) }}</span>
</template>
<ui-avatar v-if="isLocked" :user="lockedBy" class="rounded-full w-4 h-4 text-2xs" v-tooltip="lockedBy.name" />
<ui-button size="xs" inset icon="synced" variant="ghost" v-tooltip="__('messages.field_synced_with_origin')" v-if="!isReadOnly && isSyncable" v-show="isSynced" @click="desync" />
<ui-button size="xs" inset icon="unsynced" variant="ghost" v-tooltip="__('messages.field_desynced_from_origin')" v-if="!isReadOnly && isSyncable" v-show="!isSynced" @click="sync" />
</Label>
Expand All @@ -254,7 +262,7 @@ const fieldtypeComponentEvents = computed(() => ({
<div class="text-xs text-red-600" v-if="!fieldtypeComponentExists && fieldtypeComponent !== 'spacer-fieldtype'">
Component <code v-text="fieldtypeComponent"></code> does not exist.
</div>
<div :dir="direction" v-if="fieldtypeComponentExists">
<div :dir="direction" v-if="fieldtypeComponentExists" @focusin="focused" @focusout="blurred" :class="{ 'pointer-events-none select-none': isLocked }">
<Component
ref="fieldtype"
:is="fieldtypeComponent"
Expand Down
3 changes: 3 additions & 0 deletions resources/js/tests/FieldConditionsValidator.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ const Statamic = {
$dirty: {
add: () => {},
},
$events: {
$emit: () => {},
},
};
window.Statamic = Statamic;
window.__ = (msg) => msg;
Expand Down
1 change: 1 addition & 0 deletions resources/js/tests/Package.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ it('exports core', async () => {
'SaveButtonOptions',
'SortableList',
'clone',
'debounce',
'deepClone',
'requireElevatedSession',
'requireElevatedSessionIf',
Expand Down
Loading