Skip to content

Commit 0f54720

Browse files
Pro 6518 mobile preview (#4720)
* 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]>
1 parent e4d8b63 commit 0f54720

File tree

16 files changed

+471
-22
lines changed

16 files changed

+471
-22
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* Adds focus states for media library's Uploader tile
99
* Adds focus states file attachment's input UI
1010
* 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.
11+
* 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.
1112

1213
### Changes
1314

modules/@apostrophecms/admin-bar/index.js

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,56 @@ module.exports = {
1414
pageTree: true
1515
},
1616
commands(self) {
17+
const devicePreviewModeScreens = (
18+
self.apos.asset.options.devicePreviewMode?.enable &&
19+
self.apos.asset.options.devicePreviewMode?.screens
20+
) || {};
21+
const devicePreviewModeCommands = {
22+
[`${self.__meta.name}:toggle-device-preview-mode:exit`]: {
23+
type: 'item',
24+
label: {
25+
key: 'apostrophe:commandMenuToggleDevicePreviewMode',
26+
device: '$t(apostrophe:devicePreviewExit)'
27+
},
28+
action: {
29+
type: 'command-menu-admin-bar-toggle-device-preview-mode',
30+
payload: {
31+
mode: null,
32+
width: null,
33+
height: null
34+
}
35+
},
36+
shortcut: 'P,0'
37+
}
38+
};
39+
let index = 1;
40+
for (const [ name, screen ] of Object.entries(devicePreviewModeScreens)) {
41+
// Up to 9 shortcuts available
42+
if (index === 9) {
43+
break;
44+
}
45+
46+
devicePreviewModeCommands[`${self.__meta.name}:toggle-device-preview-mode:${name}`] = {
47+
type: 'item',
48+
label: {
49+
key: 'apostrophe:commandMenuToggleDevicePreviewMode',
50+
device: `$t(${screen.label})`
51+
},
52+
action: {
53+
type: 'command-menu-admin-bar-toggle-device-preview-mode',
54+
payload: {
55+
mode: name,
56+
label: `$t(${screen.label})`,
57+
width: screen.width,
58+
height: screen.height
59+
}
60+
},
61+
shortcut: `P,${index}`
62+
};
63+
64+
index += 1;
65+
};
66+
1767
return {
1868
add: {
1969
[`${self.__meta.name}:undo`]: {
@@ -63,7 +113,8 @@ module.exports = {
63113
type: 'command-menu-admin-bar-toggle-publish-draft'
64114
},
65115
shortcut: 'Ctrl+Shift+D Meta+Shift+D'
66-
}
116+
},
117+
...devicePreviewModeCommands
67118
},
68119
modal: {
69120
default: {
@@ -80,7 +131,8 @@ module.exports = {
80131
label: 'apostrophe:commandMenuMode',
81132
commands: [
82133
`${self.__meta.name}:toggle-edit-preview-mode`,
83-
`${self.__meta.name}:toggle-published-draft-document`
134+
`${self.__meta.name}:toggle-published-draft-document`,
135+
...Object.keys(devicePreviewModeCommands)
84136
]
85137
}
86138
}
@@ -355,6 +407,13 @@ module.exports = {
355407
aposLocale: context.aposLocale,
356408
aposDocId: context.aposDocId
357409
},
410+
devicePreviewMode: self.apos.asset.options.devicePreviewMode ||
411+
{
412+
enable: false,
413+
debug: false,
414+
resizable: false,
415+
screens: {}
416+
},
358417
// Base API URL appropriate to the context document
359418
contextBar: context && self.apos.doc.getManager(context.type).options.contextBar,
360419
showAdminBar: self.getShowAdminBar(req),
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
<template>
2+
<div
3+
data-apos-test="devicePreviewMode"
4+
class="apos-admin-bar__device-preview-mode"
5+
>
6+
<component
7+
:is="'AposButton'"
8+
v-for="(screen, name) in screens"
9+
:key="name"
10+
:data-apos-test="`devicePreviewMode:${name}`"
11+
:modifiers="['small', 'no-motion']"
12+
:label="screen.label"
13+
:title="$t(screen.label)"
14+
:icon="screen.icon"
15+
:icon-only="true"
16+
type="subtle"
17+
class="apos-admin-bar__device-preview-mode-button"
18+
:class="{ 'apos-is-active': mode === name }"
19+
@click="toggleDevicePreviewMode({ mode: name, label: screen.label, width: screen.width, height: screen.height })"
20+
/>
21+
</div>
22+
</template>
23+
<script>
24+
25+
export default {
26+
name: 'TheAposContextDevicePreview',
27+
props: {
28+
// { screenName: { label: string, width: string, height: string, icon: string } }
29+
screens: {
30+
type: Object,
31+
validator(value, props) {
32+
return Object.values(value).every(screen =>
33+
typeof screen.label === 'string' &&
34+
typeof screen.width === 'string' &&
35+
typeof screen.height === 'string' &&
36+
typeof screen.icon === 'string'
37+
);
38+
},
39+
default: () => {
40+
return {};
41+
}
42+
},
43+
resizable: {
44+
type: Boolean,
45+
default: false
46+
}
47+
},
48+
emits: [ 'switch-device-preview-mode', 'reset-device-preview-mode' ],
49+
data() {
50+
return {
51+
mode: null,
52+
originalBodyBackground: null
53+
};
54+
},
55+
mounted() {
56+
apos.bus.$on('command-menu-admin-bar-toggle-device-preview-mode', this.toggleDevicePreviewMode);
57+
58+
this.originalBodyBackground = window.getComputedStyle(document.querySelector('body'))?.background ||
59+
'#fff';
60+
61+
const state = this.loadState();
62+
if (state.mode) {
63+
this.toggleDevicePreviewMode(state);
64+
}
65+
},
66+
unmounted() {
67+
apos.bus.$off('command-menu-admin-bar-toggle-device-preview-mode', this.toggleDevicePreviewMode);
68+
},
69+
methods: {
70+
switchDevicePreviewMode({
71+
mode,
72+
label,
73+
width,
74+
height
75+
}) {
76+
document.querySelector('body').setAttribute('data-device-preview-mode', mode);
77+
document.querySelector('[data-apos-refreshable]').setAttribute('data-resizable', this.resizable);
78+
document.querySelector('[data-apos-refreshable]').setAttribute('data-label', this.$t(label));
79+
document.querySelector('[data-apos-refreshable]').style.width = width;
80+
document.querySelector('[data-apos-refreshable]').style.height = height;
81+
document.querySelector('[data-apos-refreshable]').style.background = this.originalBodyBackground;
82+
83+
this.mode = mode;
84+
this.$emit('switch-device-preview-mode', {
85+
mode,
86+
label,
87+
width,
88+
height
89+
});
90+
this.saveState({
91+
mode,
92+
label,
93+
width,
94+
height
95+
});
96+
},
97+
toggleDevicePreviewMode({
98+
mode,
99+
label,
100+
width,
101+
height
102+
}) {
103+
if (this.mode === mode || mode === null) {
104+
document.querySelector('body').removeAttribute('data-device-preview-mode');
105+
document.querySelector('[data-apos-refreshable]').removeAttribute('data-resizable');
106+
document.querySelector('[data-apos-refreshable]').removeAttribute('data-label');
107+
document.querySelector('[data-apos-refreshable]').style.removeProperty('width');
108+
document.querySelector('[data-apos-refreshable]').style.removeProperty('height');
109+
document.querySelector('[data-apos-refreshable]').style.removeProperty('background');
110+
111+
this.mode = null;
112+
this.$emit('reset-device-preview-mode');
113+
this.saveState({ mode: this.mode });
114+
115+
return;
116+
}
117+
118+
this.switchDevicePreviewMode({
119+
mode,
120+
label,
121+
width,
122+
height
123+
});
124+
},
125+
loadState() {
126+
return JSON.parse(sessionStorage.getItem('aposDevicePreviewMode') || '{}');
127+
},
128+
saveState({
129+
mode = null,
130+
label = null,
131+
width = null,
132+
height = null
133+
} = {}) {
134+
const state = this.loadState();
135+
if (state.mode !== mode) {
136+
sessionStorage.setItem(
137+
'aposDevicePreviewMode',
138+
JSON.stringify({
139+
mode,
140+
label,
141+
width,
142+
height
143+
})
144+
);
145+
}
146+
}
147+
}
148+
};
149+
</script>
150+
<style lang="scss" scoped>
151+
.apos-admin-bar__device-preview-mode {
152+
display: flex;
153+
gap: $spacing-half;
154+
margin-left: $spacing-double;
155+
}
156+
157+
.apos-admin-bar__device-preview-mode-button {
158+
&.apos-is-active {
159+
color: var(--a-text-primary);
160+
text-decoration: none;
161+
background-color: var(--a-base-10);
162+
border-radius: var(--a-border-radius);
163+
outline: 1px solid var(--a-base-7);
164+
}
165+
}
166+
</style>

modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextTitle.vue

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,13 @@
5050
:tooltip="tooltip"
5151
:modifiers="modifiers"
5252
/>
53+
<TheAposContextDevicePreviewMode
54+
v-if="isDevicePreviewModeEnabled"
55+
:screens="devicePreviewModeScreens"
56+
:resizable="devicePreviewModeResizable"
57+
@switch-device-preview-mode="addContextLabel"
58+
@reset-device-preview-mode="removeContextLabel"
59+
/>
5360
</span>
5461
</transition-group>
5562
</template>
@@ -94,6 +101,15 @@ export default {
94101
isUnpublished() {
95102
return !this.context.lastPublishedAt;
96103
},
104+
isDevicePreviewModeEnabled() {
105+
return this.moduleOptions.devicePreviewMode.enable || false;
106+
},
107+
devicePreviewModeScreens() {
108+
return this.moduleOptions.devicePreviewMode.screens || {};
109+
},
110+
devicePreviewModeResizable() {
111+
return this.moduleOptions.devicePreviewMode.resizable || false;
112+
},
97113
docTooltip() {
98114
return {
99115
key: 'apostrophe:lastUpdatedBy',
@@ -142,6 +158,15 @@ export default {
142158
},
143159
switchDraftMode(mode) {
144160
this.$emit('switch-draft-mode', mode);
161+
},
162+
addContextLabel({
163+
label
164+
}) {
165+
document.querySelector('[data-apos-context-label]')
166+
?.replaceChildren(document.createTextNode(this.$t(label)));
167+
},
168+
removeContextLabel() {
169+
document.querySelector('[data-apos-context-label]')?.replaceChildren();
145170
}
146171
}
147172
};

modules/@apostrophecms/asset/index.js

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,47 @@ module.exports = {
4444
rebundleModules: undefined,
4545
// In case of external front end like Astro, this option allows to
4646
// disable the build of the public UI assets.
47-
publicBundle: true
47+
publicBundle: true,
48+
// Device preview in the admin UI.
49+
// NOTE: the whole devicePreviewMode option must be carried over
50+
// to the project for override to work properly.
51+
// Nested object options are not deep merged in Apostrophe.
52+
devicePreviewMode: {
53+
// Enable device preview mode
54+
enable: false,
55+
// Warn during build about unsupported media queries.
56+
debug: false,
57+
// If we can resize the preview container?
58+
resizable: false,
59+
// Screens with icons
60+
// For adding icons, please refer to the icons documentation
61+
// https://docs.apostrophecms.org/reference/module-api/module-overview.html#icons
62+
screens: {
63+
desktop: {
64+
label: 'apostrophe:devicePreviewDesktop',
65+
width: '1500px',
66+
height: '900px',
67+
icon: 'monitor-icon'
68+
},
69+
tablet: {
70+
label: 'apostrophe:devicePreviewTablet',
71+
width: '1024px',
72+
height: '768px',
73+
icon: 'tablet-icon'
74+
},
75+
mobile: {
76+
label: 'apostrophe:devicePreviewMobile',
77+
width: '480px',
78+
height: '1000px',
79+
icon: 'cellphone-icon'
80+
}
81+
},
82+
// Transform method used on media feature
83+
// Can be either:
84+
// - (mediaFeature) => { return mediaFeature.replaceAll('xx', 'yy'); }
85+
// - null
86+
transform: null
87+
}
4888
},
4989

5090
async init(self) {

modules/@apostrophecms/asset/lib/globalIcons.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ module.exports = {
1919
'arrow-up-icon': 'ArrowUp',
2020
'binoculars-icon': 'Binoculars',
2121
'calendar-icon': 'Calendar',
22+
'cellphone-icon': 'Cellphone',
2223
'check-all-icon': 'CheckAll',
2324
'check-bold-icon': 'CheckBold',
2425
'check-circle-icon': 'CheckCircle',
@@ -98,6 +99,7 @@ module.exports = {
9899
'menu-down-icon': 'MenuDown',
99100
'minus-box-icon': 'MinusBox',
100101
'minus-icon': 'Minus',
102+
'monitor-icon': 'Monitor',
101103
'paperclip-icon': 'Paperclip',
102104
'pencil-icon': 'Pencil',
103105
'phone-icon': 'Phone',
@@ -107,6 +109,7 @@ module.exports = {
107109
'refresh-icon': 'Refresh',
108110
'shape-icon': 'Shape',
109111
'sign-text-icon': 'SignText',
112+
'tablet-icon': 'Tablet',
110113
'tag-icon': 'Tag',
111114
'text-box-icon': 'TextBox',
112115
'text-box-multiple-icon': 'TextBoxMultiple',

0 commit comments

Comments
 (0)