Skip to content

Commit

Permalink
Pro 6518 mobile preview (#4720)
Browse files Browse the repository at this point in the history
* experiment with mobile preview

* fix lint issue

* add media to container query loader

* clean up media to container queries loader

* use apostrophe options to toggle mobile preview

* add breakpoints support

* experiment with screen options

* refactor as device preview mode to avoid name conflicts with preview mode

* set todos

* rename devicePreview into devicePreviewMode and validate screens

* set active style

* set active style and keyboard shortcuts

* restore previous filename logic

* revert plugins

* note about deep merge

* update changelog

* remove test styles

* fix mixed declaration warnings

* translate device

* translate additional keys in keyboard shortcut label

* add transform function

* explain available transform options

* up to 9 shortcuts

* use flexbox

* rename devicePreviewMode options

* add label

* fix lint issue

* support preview mode background

* style tweaks

* lint

* save device preview state, transition only for non-resizable containers

* save device preview mode state inside own component

* update background on body

* keep state

* replace background and not just color

* add selector prefix for mobile first responsive

* add empty lines

* exclude content with \n from being replaced

* match only single escaped backslash

* update transform example

* improve css specificity for media queries

* do not increase specificity with where

* simplify regex for container query and use postcss to parse media and container query

* set body to media query

* add context label

---------

Co-authored-by: Harouna Traoré <[email protected]>
Co-authored-by: Stuart Romanek <[email protected]>
  • Loading branch information
3 people authored Oct 2, 2024
1 parent e4d8b63 commit 0f54720
Show file tree
Hide file tree
Showing 16 changed files with 471 additions and 22 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* Adds focus states for media library's Uploader tile
* Adds focus states file attachment's input UI
* Simplified importing rich text widgets via the REST API. If you you have HTML that contains `img` tags pointing to existing images, you can now import them all quickly. When supplying the rich text widget object, include an `import` property with an `html` subproperty, rather than the usual `content` property. You can optionally provide a `baseUrl` subproperty as well. Any images present in `html` will be imported automatically and the correct `figure` tags will be added to the new rich text widget, along with any other markup acceptable to the widget's configuration.
* Add mobile preview feature to the admin UI. The feature can be enabled using the `@apostrophecms/asset` module new `devicePreviewMode` option. Once enabled, the asset build process will duplicate existing media queries as container queries. There are some limitations in the equivalence media queries / container queries. You can refer to the [CSS @container at-rule](https://developer.mozilla.org/en-US/docs/Web/CSS/@container) documentation for more information. You can also enable `devicePreviewMode.debug` to be notified in the console when the build encounter an unsupported media query.

### Changes

Expand Down
63 changes: 61 additions & 2 deletions modules/@apostrophecms/admin-bar/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,56 @@ module.exports = {
pageTree: true
},
commands(self) {
const devicePreviewModeScreens = (
self.apos.asset.options.devicePreviewMode?.enable &&
self.apos.asset.options.devicePreviewMode?.screens
) || {};
const devicePreviewModeCommands = {
[`${self.__meta.name}:toggle-device-preview-mode:exit`]: {
type: 'item',
label: {
key: 'apostrophe:commandMenuToggleDevicePreviewMode',
device: '$t(apostrophe:devicePreviewExit)'
},
action: {
type: 'command-menu-admin-bar-toggle-device-preview-mode',
payload: {
mode: null,
width: null,
height: null
}
},
shortcut: 'P,0'
}
};
let index = 1;
for (const [ name, screen ] of Object.entries(devicePreviewModeScreens)) {
// Up to 9 shortcuts available
if (index === 9) {
break;
}

devicePreviewModeCommands[`${self.__meta.name}:toggle-device-preview-mode:${name}`] = {
type: 'item',
label: {
key: 'apostrophe:commandMenuToggleDevicePreviewMode',
device: `$t(${screen.label})`
},
action: {
type: 'command-menu-admin-bar-toggle-device-preview-mode',
payload: {
mode: name,
label: `$t(${screen.label})`,
width: screen.width,
height: screen.height
}
},
shortcut: `P,${index}`
};

index += 1;
};

return {
add: {
[`${self.__meta.name}:undo`]: {
Expand Down Expand Up @@ -63,7 +113,8 @@ module.exports = {
type: 'command-menu-admin-bar-toggle-publish-draft'
},
shortcut: 'Ctrl+Shift+D Meta+Shift+D'
}
},
...devicePreviewModeCommands
},
modal: {
default: {
Expand All @@ -80,7 +131,8 @@ module.exports = {
label: 'apostrophe:commandMenuMode',
commands: [
`${self.__meta.name}:toggle-edit-preview-mode`,
`${self.__meta.name}:toggle-published-draft-document`
`${self.__meta.name}:toggle-published-draft-document`,
...Object.keys(devicePreviewModeCommands)
]
}
}
Expand Down Expand Up @@ -355,6 +407,13 @@ module.exports = {
aposLocale: context.aposLocale,
aposDocId: context.aposDocId
},
devicePreviewMode: self.apos.asset.options.devicePreviewMode ||
{
enable: false,
debug: false,
resizable: false,
screens: {}
},
// Base API URL appropriate to the context document
contextBar: context && self.apos.doc.getManager(context.type).options.contextBar,
showAdminBar: self.getShowAdminBar(req),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
<template>
<div
data-apos-test="devicePreviewMode"
class="apos-admin-bar__device-preview-mode"
>
<component
:is="'AposButton'"
v-for="(screen, name) in screens"
:key="name"
:data-apos-test="`devicePreviewMode:${name}`"
:modifiers="['small', 'no-motion']"
:label="screen.label"
:title="$t(screen.label)"
:icon="screen.icon"
:icon-only="true"
type="subtle"
class="apos-admin-bar__device-preview-mode-button"
:class="{ 'apos-is-active': mode === name }"
@click="toggleDevicePreviewMode({ mode: name, label: screen.label, width: screen.width, height: screen.height })"
/>
</div>
</template>
<script>
export default {
name: 'TheAposContextDevicePreview',
props: {
// { screenName: { label: string, width: string, height: string, icon: string } }
screens: {
type: Object,
validator(value, props) {
return Object.values(value).every(screen =>
typeof screen.label === 'string' &&
typeof screen.width === 'string' &&
typeof screen.height === 'string' &&
typeof screen.icon === 'string'
);
},
default: () => {
return {};
}
},
resizable: {
type: Boolean,
default: false
}
},
emits: [ 'switch-device-preview-mode', 'reset-device-preview-mode' ],
data() {
return {
mode: null,
originalBodyBackground: null
};
},
mounted() {
apos.bus.$on('command-menu-admin-bar-toggle-device-preview-mode', this.toggleDevicePreviewMode);
this.originalBodyBackground = window.getComputedStyle(document.querySelector('body'))?.background ||
'#fff';
const state = this.loadState();
if (state.mode) {
this.toggleDevicePreviewMode(state);
}
},
unmounted() {
apos.bus.$off('command-menu-admin-bar-toggle-device-preview-mode', this.toggleDevicePreviewMode);
},
methods: {
switchDevicePreviewMode({
mode,
label,
width,
height
}) {
document.querySelector('body').setAttribute('data-device-preview-mode', mode);
document.querySelector('[data-apos-refreshable]').setAttribute('data-resizable', this.resizable);
document.querySelector('[data-apos-refreshable]').setAttribute('data-label', this.$t(label));
document.querySelector('[data-apos-refreshable]').style.width = width;
document.querySelector('[data-apos-refreshable]').style.height = height;
document.querySelector('[data-apos-refreshable]').style.background = this.originalBodyBackground;
this.mode = mode;
this.$emit('switch-device-preview-mode', {
mode,
label,
width,
height
});
this.saveState({
mode,
label,
width,
height
});
},
toggleDevicePreviewMode({
mode,
label,
width,
height
}) {
if (this.mode === mode || mode === null) {
document.querySelector('body').removeAttribute('data-device-preview-mode');
document.querySelector('[data-apos-refreshable]').removeAttribute('data-resizable');
document.querySelector('[data-apos-refreshable]').removeAttribute('data-label');
document.querySelector('[data-apos-refreshable]').style.removeProperty('width');
document.querySelector('[data-apos-refreshable]').style.removeProperty('height');
document.querySelector('[data-apos-refreshable]').style.removeProperty('background');
this.mode = null;
this.$emit('reset-device-preview-mode');
this.saveState({ mode: this.mode });
return;
}
this.switchDevicePreviewMode({
mode,
label,
width,
height
});
},
loadState() {
return JSON.parse(sessionStorage.getItem('aposDevicePreviewMode') || '{}');
},
saveState({
mode = null,
label = null,
width = null,
height = null
} = {}) {
const state = this.loadState();
if (state.mode !== mode) {
sessionStorage.setItem(
'aposDevicePreviewMode',
JSON.stringify({
mode,
label,
width,
height
})
);
}
}
}
};
</script>
<style lang="scss" scoped>
.apos-admin-bar__device-preview-mode {
display: flex;
gap: $spacing-half;
margin-left: $spacing-double;
}
.apos-admin-bar__device-preview-mode-button {
&.apos-is-active {
color: var(--a-text-primary);
text-decoration: none;
background-color: var(--a-base-10);
border-radius: var(--a-border-radius);
outline: 1px solid var(--a-base-7);
}
}
</style>
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@
:tooltip="tooltip"
:modifiers="modifiers"
/>
<TheAposContextDevicePreviewMode
v-if="isDevicePreviewModeEnabled"
:screens="devicePreviewModeScreens"
:resizable="devicePreviewModeResizable"
@switch-device-preview-mode="addContextLabel"
@reset-device-preview-mode="removeContextLabel"
/>
</span>
</transition-group>
</template>
Expand Down Expand Up @@ -94,6 +101,15 @@ export default {
isUnpublished() {
return !this.context.lastPublishedAt;
},
isDevicePreviewModeEnabled() {
return this.moduleOptions.devicePreviewMode.enable || false;
},
devicePreviewModeScreens() {
return this.moduleOptions.devicePreviewMode.screens || {};
},
devicePreviewModeResizable() {
return this.moduleOptions.devicePreviewMode.resizable || false;
},
docTooltip() {
return {
key: 'apostrophe:lastUpdatedBy',
Expand Down Expand Up @@ -142,6 +158,15 @@ export default {
},
switchDraftMode(mode) {
this.$emit('switch-draft-mode', mode);
},
addContextLabel({
label
}) {
document.querySelector('[data-apos-context-label]')
?.replaceChildren(document.createTextNode(this.$t(label)));
},
removeContextLabel() {
document.querySelector('[data-apos-context-label]')?.replaceChildren();
}
}
};
Expand Down
42 changes: 41 additions & 1 deletion modules/@apostrophecms/asset/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,47 @@ module.exports = {
rebundleModules: undefined,
// In case of external front end like Astro, this option allows to
// disable the build of the public UI assets.
publicBundle: true
publicBundle: true,
// Device preview in the admin UI.
// NOTE: the whole devicePreviewMode option must be carried over
// to the project for override to work properly.
// Nested object options are not deep merged in Apostrophe.
devicePreviewMode: {
// Enable device preview mode
enable: false,
// Warn during build about unsupported media queries.
debug: false,
// If we can resize the preview container?
resizable: false,
// Screens with icons
// For adding icons, please refer to the icons documentation
// https://docs.apostrophecms.org/reference/module-api/module-overview.html#icons
screens: {
desktop: {
label: 'apostrophe:devicePreviewDesktop',
width: '1500px',
height: '900px',
icon: 'monitor-icon'
},
tablet: {
label: 'apostrophe:devicePreviewTablet',
width: '1024px',
height: '768px',
icon: 'tablet-icon'
},
mobile: {
label: 'apostrophe:devicePreviewMobile',
width: '480px',
height: '1000px',
icon: 'cellphone-icon'
}
},
// Transform method used on media feature
// Can be either:
// - (mediaFeature) => { return mediaFeature.replaceAll('xx', 'yy'); }
// - null
transform: null
}
},

async init(self) {
Expand Down
3 changes: 3 additions & 0 deletions modules/@apostrophecms/asset/lib/globalIcons.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ module.exports = {
'arrow-up-icon': 'ArrowUp',
'binoculars-icon': 'Binoculars',
'calendar-icon': 'Calendar',
'cellphone-icon': 'Cellphone',
'check-all-icon': 'CheckAll',
'check-bold-icon': 'CheckBold',
'check-circle-icon': 'CheckCircle',
Expand Down Expand Up @@ -98,6 +99,7 @@ module.exports = {
'menu-down-icon': 'MenuDown',
'minus-box-icon': 'MinusBox',
'minus-icon': 'Minus',
'monitor-icon': 'Monitor',
'paperclip-icon': 'Paperclip',
'pencil-icon': 'Pencil',
'phone-icon': 'Phone',
Expand All @@ -107,6 +109,7 @@ module.exports = {
'refresh-icon': 'Refresh',
'shape-icon': 'Shape',
'sign-text-icon': 'SignText',
'tablet-icon': 'Tablet',
'tag-icon': 'Tag',
'text-box-icon': 'TextBox',
'text-box-multiple-icon': 'TextBoxMultiple',
Expand Down
Loading

0 comments on commit 0f54720

Please sign in to comment.