apostrophe 4.27.1 → 4.28.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +15 -0
- package/CHANGELOG.md +40 -0
- package/README.md +142 -0
- package/index.js +3 -0
- package/lib/stream-proxy.js +49 -0
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextTitle.vue +2 -11
- package/modules/@apostrophecms/area/ui/apos/apps/AposAreas.js +38 -6
- package/modules/@apostrophecms/area/ui/apos/components/AposAreaEditor.vue +12 -1
- package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +111 -41
- package/modules/@apostrophecms/area/ui/apos/components/AposBreadcrumbOperations.vue +1 -0
- package/modules/@apostrophecms/area/ui/apos/components/AposWidgetControls.vue +22 -10
- package/modules/@apostrophecms/area/ui/apos/logic/AposAreaEditor.js +40 -0
- package/modules/@apostrophecms/asset/index.js +3 -2
- package/modules/@apostrophecms/attachment/index.js +270 -0
- package/modules/@apostrophecms/doc/index.js +8 -2
- package/modules/@apostrophecms/doc-type/index.js +81 -1
- package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +18 -2
- package/modules/@apostrophecms/express/index.js +30 -1
- package/modules/@apostrophecms/file/index.js +70 -6
- package/modules/@apostrophecms/i18n/index.js +20 -1
- package/modules/@apostrophecms/image/index.js +11 -0
- package/modules/@apostrophecms/layout-widget/ui/apos/components/AposAreaLayoutEditor.vue +31 -6
- package/modules/@apostrophecms/layout-widget/ui/apos/components/AposGridLayout.vue +12 -10
- package/modules/@apostrophecms/login/index.js +43 -11
- package/modules/@apostrophecms/modal/ui/apos/components/AposDocsManagerToolbar.vue +2 -1
- package/modules/@apostrophecms/modal/ui/apos/components/AposModal.vue +5 -0
- package/modules/@apostrophecms/page/index.js +9 -11
- package/modules/@apostrophecms/page-type/index.js +6 -1
- package/modules/@apostrophecms/piece-page-type/index.js +100 -13
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposImageControlDialog.vue +1 -0
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +28 -12
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapLink.vue +1 -0
- package/modules/@apostrophecms/schema/ui/apos/components/AposSearchList.vue +1 -1
- package/modules/@apostrophecms/styles/lib/apiRoutes.js +25 -5
- package/modules/@apostrophecms/styles/lib/handlers.js +19 -0
- package/modules/@apostrophecms/styles/lib/methods.js +35 -12
- package/modules/@apostrophecms/styles/ui/apos/components/TheAposStyles.vue +7 -2
- package/modules/@apostrophecms/task/index.js +9 -1
- package/modules/@apostrophecms/template/views/outerLayoutBase.html +3 -0
- package/modules/@apostrophecms/ui/index.js +2 -0
- package/modules/@apostrophecms/ui/ui/apos/components/AposButtonGroup.vue +1 -1
- package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenu.vue +5 -0
- package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenuDialog.vue +5 -0
- package/modules/@apostrophecms/ui/ui/apos/lib/vue.js +2 -0
- package/modules/@apostrophecms/ui/ui/apos/stores/widget.js +12 -7
- package/modules/@apostrophecms/ui/ui/apos/stores/widgetGraph.js +461 -0
- package/modules/@apostrophecms/ui/ui/apos/universal/graph.js +452 -0
- package/modules/@apostrophecms/ui/ui/apos/universal/widgetGraph.js +10 -0
- package/modules/@apostrophecms/uploadfs/index.js +15 -1
- package/modules/@apostrophecms/url/index.js +419 -1
- package/package.json +5 -5
- package/test/add-missing-schema-fields-project/node_modules/.package-lock.json +131 -0
- package/test/external-front.js +1 -0
- package/test/files.js +264 -0
- package/test/login-requirements.js +145 -3
- package/test/static-build.js +2701 -0
- package/test/universal-graph.js +1135 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(timeout 180 npx mocha:*)",
|
|
5
|
+
"Bash(timeout 600 npx mocha:*)",
|
|
6
|
+
"Bash(npm ls:*)",
|
|
7
|
+
"Bash(timeout 540 npx mocha:*)",
|
|
8
|
+
"Bash(echo:*)",
|
|
9
|
+
"Bash(timeout 10 node:*)",
|
|
10
|
+
"Bash(timeout 300 npx mocha:*)",
|
|
11
|
+
"Bash(timeout 60 npx mocha:*)",
|
|
12
|
+
"Bash(timeout 120 npx mocha:*)"
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.28.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- f8d1952: Bug fix: the "pretty URLs" feature of @apostrophecms/file is now compatible with locale prefixes.
|
|
8
|
+
|
|
9
|
+
## 4.28.0
|
|
10
|
+
|
|
11
|
+
### Adds
|
|
12
|
+
|
|
13
|
+
- Adds support for static URLs and static external frontend builds.
|
|
14
|
+
- Adds widget graph store, accessible in Admin UI.
|
|
15
|
+
- Support for the new `prettyUrls: true` option for @apostrophecms/file, which enables "pretty URLs" for PDFs and other items in the file library, in exchange for a small performance impact. Edit the slug field to adjust the pretty URL
|
|
16
|
+
|
|
17
|
+
### Fixes
|
|
18
|
+
|
|
19
|
+
- Fix a bug when rich text link open in new tab checkbox can't be cleared
|
|
20
|
+
- Ensures the `getOne` API can correctly retrieve documents that are not localized. Thanks to [Eduardo Correal](https://github.com/ecb34).
|
|
21
|
+
- Clicking a choice should dismiss the relationship suggestions dropdown. This regression was caused by part of the patch in release 4.27.1.
|
|
22
|
+
- lint on main
|
|
23
|
+
- Adds inner wrapper to area widget that creates a separate z-index context for the user, fixing widget/apostrophe UI conflicts
|
|
24
|
+
- Fixes a bug where Layout's mode erroneously switches state
|
|
25
|
+
- Fixes a bug where Layouts synthetic column styles were not reaching their children
|
|
26
|
+
- Fixes styles being sanitized as html (breaks quotes) and resolves duplicate styles on the page.
|
|
27
|
+
- Fix subtle bug in AposPermissionGrid that caused unrelated clicks to be "swallowed" due to a race condition at low network speeds
|
|
28
|
+
- Fixes two conditions where slow internet speed could cause input to lose focus before a selection can be registered.
|
|
29
|
+
|
|
30
|
+
### Changes
|
|
31
|
+
|
|
32
|
+
- Improve re-rendering UX while keeping the performance optimization
|
|
33
|
+
- raise the user's widget z-index context only when focused
|
|
34
|
+
- Hide add content buttons on rich text editing, like widget controls
|
|
35
|
+
- Refine in-context focus states for calmer UX
|
|
36
|
+
- Simplifies some in-context UI rendering checks
|
|
37
|
+
- Updated dependencies
|
|
38
|
+
|
|
39
|
+
### Security
|
|
40
|
+
|
|
41
|
+
- This previously undisclosed security vulnerability allowed users who had compromised a password to perform actions in the CMS without 2FA. For sites not using 2FA (e.g. our @apostrophecms/login-totp module), this changes nothing. But for those using our TOTP module or similar, upgrading to this release is urgent. Thanks to 0xkakashi for reporting the issue and recommending a fix.
|
|
42
|
+
|
|
3
43
|
## 4.27.1 (2026-03-03)
|
|
4
44
|
|
|
5
45
|
### Fixes
|
package/README.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<a href="https://github.com/apostrophecms/apostrophe">
|
|
3
|
+
<img src="logo.svg" alt="ApostropheCMS logo" width="80" height="80">
|
|
4
|
+
</a>
|
|
5
|
+
|
|
6
|
+
<h1>ApostropheCMS</h1>
|
|
7
|
+
|
|
8
|
+
<p>
|
|
9
|
+
<a aria-label="Join the community on Discord" href="http://chat.apostrophecms.org">
|
|
10
|
+
<img alt="" src="https://img.shields.io/discord/517772094482677790?color=5865f2&label=Join%20the%20Discord&logo=discord&logoColor=fff&labelColor=000&style=for-the-badge&logoWidth=20" />
|
|
11
|
+
</a>
|
|
12
|
+
<a aria-label="License" href="https://github.com/apostrophecms/apostrophe/blob/main/LICENSE.md">
|
|
13
|
+
<img alt="" src="https://img.shields.io/static/v1?style=for-the-badge&labelColor=000000&label=License&message=MIT&color=3DA639" />
|
|
14
|
+
</a>
|
|
15
|
+
</p>
|
|
16
|
+
|
|
17
|
+
<p>
|
|
18
|
+
<strong>Full-stack CMS for developers and content teams</strong><br />
|
|
19
|
+
Build websites with in-context editing and headless flexibility using Node.js and MongoDB.
|
|
20
|
+
<br />
|
|
21
|
+
<a href="https://docs.apostrophecms.org/"><strong>Documentation »</strong></a>
|
|
22
|
+
<br />
|
|
23
|
+
<br />
|
|
24
|
+
<a href="http://demo.apostrophecms.com">Demo</a>
|
|
25
|
+
·
|
|
26
|
+
<a href="https://roadmap.apostrophecms.com/roadmap">Roadmap</a>
|
|
27
|
+
·
|
|
28
|
+
<a href="https://github.com/apostrophecms/apostrophe/issues/new?assignees=&labels=bug,3.0&template=bug_report.md&title=">Report Bug</a>
|
|
29
|
+
</p>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
## About
|
|
33
|
+
|
|
34
|
+
ApostropheCMS is a full-stack content management system built with Node.js and MongoDB. Content creators can edit directly on live pages without switching between admin interfaces, while developers can build with modern JavaScript or use it headlessly with any frontend framework.
|
|
35
|
+
|
|
36
|
+
### Key Features
|
|
37
|
+
|
|
38
|
+
- **🎯 In-Context Editing** - Content creators edit directly on the live page, seeing changes instantly
|
|
39
|
+
- **⚡ Headless-Ready** - Use any frontend framework while keeping the powerful admin experience
|
|
40
|
+
- **🛠️ Developer-First** - Built with Node.js and MongoDB for full-stack JavaScript development
|
|
41
|
+
- **📈 Scales Beautifully** - From small sites to enterprise applications handling millions of pages
|
|
42
|
+
- **🔐 Enterprise Features** - Advanced permissions, workflow management, automated translations, and more
|
|
43
|
+
|
|
44
|
+
## System Requirements
|
|
45
|
+
|
|
46
|
+
| Requirement | Version | Installation Notes |
|
|
47
|
+
|-------------|---------|-------------------|
|
|
48
|
+
| **Node.js** | 20.x+ | Use [NVM](https://github.com/nvm-sh/nvm) for version management |
|
|
49
|
+
| **MongoDB** | 6.0+ | [MongoDB Atlas](https://www.mongodb.com/atlas) (cloud) or local install |
|
|
50
|
+
| **npm** | 10.x+ | Included with Node.js |
|
|
51
|
+
|
|
52
|
+
See our [setup guides](https://docs.apostrophecms.org/guide/development-setup.html) for installation instructions.
|
|
53
|
+
|
|
54
|
+
## Quick Start
|
|
55
|
+
|
|
56
|
+
Get ApostropheCMS running locally in minutes:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# Option 1: Install CLI globally (recommended for multiple projects)
|
|
60
|
+
npm install -g @apostrophecms/cli
|
|
61
|
+
apos create my-website
|
|
62
|
+
cd my-website
|
|
63
|
+
npm run dev
|
|
64
|
+
|
|
65
|
+
# Option 2: Use npx for one-time project creation
|
|
66
|
+
npx @apostrophecms/cli create my-website
|
|
67
|
+
cd my-website
|
|
68
|
+
npm run dev
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Your new ApostropheCMS site will be available at `http://localhost:3000` with a powerful admin interface at `/login`.
|
|
72
|
+
|
|
73
|
+
### Prefer to Go Headless?
|
|
74
|
+
|
|
75
|
+
**Get started with Astro integration** - the easiest way to build headless sites while keeping visual editing:
|
|
76
|
+
|
|
77
|
+
- **[Apollo Starter Kit (Astro)](https://apostrophecms.com/starter-kits/apollo-starter-kit-for-astro-cms)** - Production-ready foundation with beautiful design system and rich content features
|
|
78
|
+
- **[Essentials Starter Kit (Astro)](git clone https://github.com/apostrophecms/starter-kit-astro-essentials)** - Minimal, clean foundation for building custom designs from scratch
|
|
79
|
+
|
|
80
|
+
Both starter kits provide headless CMS power with in-context editing, letting content creators edit directly on the live site while you build with modern frontend tools. Our Astro integration handles all the content fetching automatically—no REST API calls to write.
|
|
81
|
+
|
|
82
|
+
**Desire a different frontend framework?** Use our REST APIs with React, Vue, Svelte, or any other framework:
|
|
83
|
+
|
|
84
|
+
- **[REST API Documentation](https://docs.apostrophecms.org/reference/api/pieces.html)** - Complete API reference
|
|
85
|
+
- **[Headless CMS Guide](https://docs.apostrophecms.org/guide/headless-cms.html)** - Integration walkthrough for any framework
|
|
86
|
+
|
|
87
|
+
### Hosting & Deployment
|
|
88
|
+
|
|
89
|
+
Choose [ApostropheCMS hosting](https://apostrophecms.com/hosting) for turnkey solutions with optimized performance and dedicated support, or deploy to [any platform where Node.js runs](https://docs.apostrophecms.org/guide/hosting.html).
|
|
90
|
+
|
|
91
|
+
## Built With Modern Tech
|
|
92
|
+
|
|
93
|
+
- **[Node.js](https://nodejs.org/)** - JavaScript runtime for server-side development
|
|
94
|
+
- **[MongoDB](https://www.mongodb.com/)** - Flexible document database for content storage
|
|
95
|
+
- **ESM Modules** - Native ES6 module support for modern JavaScript
|
|
96
|
+
- **Vite** - Lightning-fast build tool and development server
|
|
97
|
+
- **Modern JavaScript** - ES6+, async/await, and contemporary development patterns
|
|
98
|
+
|
|
99
|
+
## Community & Support
|
|
100
|
+
|
|
101
|
+
**Join other developers and content creators using ApostropheCMS:**
|
|
102
|
+
|
|
103
|
+
- **[Discord](https://discord.com/invite/XkbRNq7)** - Get help, share projects, and connect with other users
|
|
104
|
+
- **[GitHub Discussions](https://github.com/apostrophecms/apostrophe/discussions)** - Feature requests, technical discussions, and community support
|
|
105
|
+
- **[Documentation](https://docs.apostrophecms.org/)** - Comprehensive guides, tutorials, and API references
|
|
106
|
+
|
|
107
|
+
## Contributing
|
|
108
|
+
|
|
109
|
+
We welcome contributions from the community! Whether you're fixing bugs, adding features, or improving documentation, your help makes ApostropheCMS better for everyone.
|
|
110
|
+
|
|
111
|
+
- **[Contribution Guide](https://github.com/apostrophecms/apostrophe/blob/main/CONTRIBUTING.md)** - How to contribute code, documentation, and feedback
|
|
112
|
+
- **[Good First Issues](https://github.com/apostrophecms/apostrophe/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)** - Perfect starting points for new contributors
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
## Pro Features
|
|
116
|
+
|
|
117
|
+
**For teams and organizations requiring additional features:**
|
|
118
|
+
|
|
119
|
+
- **🔐 Advanced User Management** - Granular permissions, user groups, and access controls
|
|
120
|
+
- **🌍 Automated Translation** - AI-powered translation with DeepL, Google Translate, and Azure
|
|
121
|
+
- **📊 Analytics & SEO** - Built-in SEO optimization and content analytics
|
|
122
|
+
- **⚡ Performance Optimization** - Advanced caching, CDN integration, and performance monitoring
|
|
123
|
+
- **🏢 Multisite Management** - Manage multiple sites from a single dashboard with shared resources
|
|
124
|
+
- **💼 Professional Support** - Dedicated support, training, and consultation services
|
|
125
|
+
|
|
126
|
+
[Explore all the pro extensions](https://apostrophecms.com/extensions?autocomplete=&license=assembly&license=pro) and [sign up](https://app.apostrophecms.com/login) for a Pro license in our self-service Apostrophe Workspaces, or [contact us](https://apostrophecms.com/contact-us) to learn about licensing and support options.
|
|
127
|
+
|
|
128
|
+
## License
|
|
129
|
+
|
|
130
|
+
ApostropheCMS is open source software licensed under the [MIT License](https://github.com/apostrophecms/apostrophe/blob/main/LICENSE.md). This means you're free to use, modify, and distribute it for both personal and commercial projects.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
<div align="center">
|
|
135
|
+
<p>
|
|
136
|
+
<strong>Ready to build something amazing?</strong><br>
|
|
137
|
+
<a href="https://docs.apostrophecms.org/">Get started with our documentation</a> or <a href="https://apostrophecms.com/contact-us">talk to our team</a>
|
|
138
|
+
</p>
|
|
139
|
+
<p>
|
|
140
|
+
<em>Built with ❤️ by the <a href="https://apostrophecms.com">ApostropheCMS team</a></em>
|
|
141
|
+
</p>
|
|
142
|
+
</div>
|
package/index.js
CHANGED
|
@@ -479,6 +479,9 @@ async function apostrophe(options, telemetry, rootSpan) {
|
|
|
479
479
|
// Environment variable override
|
|
480
480
|
self.options.baseUrl = process.env.APOS_BASE_URL || self.options.baseUrl;
|
|
481
481
|
self.baseUrl = self.options.baseUrl;
|
|
482
|
+
self.options.staticBaseUrl = process.env.APOS_STATIC_BASE_URL ||
|
|
483
|
+
self.options.staticBaseUrl;
|
|
484
|
+
self.staticBaseUrl = self.options.staticBaseUrl;
|
|
482
485
|
self.prefix = self.options.prefix || '';
|
|
483
486
|
}
|
|
484
487
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Make a request for url and stream it to req.res,
|
|
2
|
+
// passing through relevant headers, without downloading
|
|
3
|
+
// the whole thing to disk. Site-relative URLs are
|
|
4
|
+
// resolved via req.baseUrl. Server-side errors are logged to
|
|
5
|
+
// error() which should accept multiple arguments
|
|
6
|
+
|
|
7
|
+
module.exports = async function(req, url, { error }) {
|
|
8
|
+
const res = req.res;
|
|
9
|
+
if (url.startsWith('/')) {
|
|
10
|
+
// Can't make a self-request without an absolute URL
|
|
11
|
+
url = `${req.baseUrl}${url}`;
|
|
12
|
+
}
|
|
13
|
+
let response;
|
|
14
|
+
try {
|
|
15
|
+
response = await fetch(url);
|
|
16
|
+
} catch (e) {
|
|
17
|
+
return send502(e);
|
|
18
|
+
}
|
|
19
|
+
for (const header of [ 'content-type', 'etag', 'last-modified', 'content-disposition', 'cache-control' ]) {
|
|
20
|
+
const result = response.headers.get(header);
|
|
21
|
+
if (result != null) {
|
|
22
|
+
res.header(header, result);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
res.status(response.status);
|
|
26
|
+
if (response.body == null) {
|
|
27
|
+
return res.end();
|
|
28
|
+
}
|
|
29
|
+
response.body.pipeTo(new WritableStream({
|
|
30
|
+
write(chunk) {
|
|
31
|
+
res.write(chunk);
|
|
32
|
+
},
|
|
33
|
+
close() {
|
|
34
|
+
res.end();
|
|
35
|
+
},
|
|
36
|
+
abort(reason) {
|
|
37
|
+
if (!res.headersSent) {
|
|
38
|
+
return send502(reason);
|
|
39
|
+
} else {
|
|
40
|
+
// Only way to signal failure after headers are sent
|
|
41
|
+
res.destroy();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}));
|
|
45
|
+
function send502(e) {
|
|
46
|
+
error(`Error fetching "ugly URL" ${url} to resolve pretty URL ${req.url}:`, e);
|
|
47
|
+
return res.status(502).send('upstream media error fetching data for pretty URL');
|
|
48
|
+
}
|
|
49
|
+
};
|
|
@@ -166,22 +166,13 @@ export default {
|
|
|
166
166
|
color: var(--a-text-primary);
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
-
&__document-title {
|
|
170
|
-
margin-top: 1px;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
169
|
&__separator {
|
|
174
170
|
align-items: center;
|
|
175
|
-
margin-top: 1px;
|
|
176
171
|
padding: 0 7px;
|
|
177
172
|
}
|
|
178
173
|
|
|
179
|
-
&__document {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
:deep(.apos-context-menu__items) {
|
|
183
|
-
min-width: 150px;
|
|
184
|
-
}
|
|
174
|
+
&__document :deep(.apos-context-menu__items) {
|
|
175
|
+
min-width: 150px;
|
|
185
176
|
}
|
|
186
177
|
}
|
|
187
178
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
|
|
2
|
-
import createApp from 'Modules/@apostrophecms/ui/lib/vue';
|
|
2
|
+
import createApp, { pinia } from 'Modules/@apostrophecms/ui/lib/vue';
|
|
3
|
+
import { useWidgetGraphStore } from 'Modules/@apostrophecms/ui/stores/widgetGraph.js';
|
|
3
4
|
import { nextTick } from 'vue';
|
|
4
5
|
|
|
5
6
|
export default function() {
|
|
@@ -24,6 +25,12 @@ export default function() {
|
|
|
24
25
|
});
|
|
25
26
|
|
|
26
27
|
apos.bus.$on('refreshed', function() {
|
|
28
|
+
// Re-instantiate the on-page widget graph before remounting areas.
|
|
29
|
+
// The normal mounted hooks will rebuild it from fresh data.
|
|
30
|
+
const graphStore = useWidgetGraphStore(pinia);
|
|
31
|
+
if (apos.adminBar?.contextId) {
|
|
32
|
+
graphStore.resetGraph(apos.adminBar.contextId);
|
|
33
|
+
}
|
|
27
34
|
createAreaAppsAndRunPlayersIfDone();
|
|
28
35
|
});
|
|
29
36
|
|
|
@@ -117,10 +124,18 @@ export default function() {
|
|
|
117
124
|
|
|
118
125
|
el.parentNode.replaceChild(apos.area.activeEditor.$el, el);
|
|
119
126
|
} else {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
127
|
+
const rect = el.getBoundingClientRect();
|
|
128
|
+
const isInViewport = rect.bottom >= 0 &&
|
|
129
|
+
rect.top <= window.innerHeight;
|
|
130
|
+
|
|
131
|
+
if (isInViewport) {
|
|
132
|
+
mountApp();
|
|
133
|
+
} else {
|
|
134
|
+
observer = new IntersectionObserver(observed, {
|
|
135
|
+
rootMargin: '600px'
|
|
136
|
+
});
|
|
137
|
+
observer.observe(el);
|
|
138
|
+
}
|
|
124
139
|
}
|
|
125
140
|
|
|
126
141
|
function observed(entries) {
|
|
@@ -129,8 +144,14 @@ export default function() {
|
|
|
129
144
|
return;
|
|
130
145
|
}
|
|
131
146
|
if (created) {
|
|
147
|
+
observer.disconnect();
|
|
132
148
|
return;
|
|
133
149
|
}
|
|
150
|
+
mountApp();
|
|
151
|
+
observer.disconnect();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function mountApp() {
|
|
134
155
|
const app = createApp(component, {
|
|
135
156
|
options,
|
|
136
157
|
id: data._id,
|
|
@@ -142,10 +163,21 @@ export default function() {
|
|
|
142
163
|
parentOptions,
|
|
143
164
|
renderings
|
|
144
165
|
});
|
|
166
|
+
|
|
167
|
+
// Resolve graphKey: if this area is inside a modal that owns a
|
|
168
|
+
// graph (data-apos-graph-key), use that key. Otherwise fall back
|
|
169
|
+
// to the on-page contextId. This single DOM lookup bridges the
|
|
170
|
+
// provide/inject gap created by createApp.
|
|
171
|
+
const graphKey = el.closest('[data-apos-graph-key]')
|
|
172
|
+
?.getAttribute('data-apos-graph-key') || apos.adminBar?.contextId || null;
|
|
173
|
+
// Provide the resolved graphKey so every descendant component
|
|
174
|
+
// can simply inject('aposGraphKey') and get the correct value.
|
|
175
|
+
if (graphKey) {
|
|
176
|
+
app.provide('aposGraphKey', graphKey);
|
|
177
|
+
}
|
|
145
178
|
app.mount(el);
|
|
146
179
|
mountedApps.set(el, app);
|
|
147
180
|
created = true;
|
|
148
|
-
observer.disconnect();
|
|
149
181
|
}
|
|
150
182
|
}
|
|
151
183
|
|
|
@@ -3,7 +3,12 @@
|
|
|
3
3
|
v-click-outside-element="resetFocusedArea"
|
|
4
4
|
:data-apos-area="areaId"
|
|
5
5
|
class="apos-area"
|
|
6
|
-
:class="
|
|
6
|
+
:class="[
|
|
7
|
+
themeClass,
|
|
8
|
+
{
|
|
9
|
+
'apos-area--empty': next.length === 0
|
|
10
|
+
}
|
|
11
|
+
]"
|
|
7
12
|
@click="setFocusedArea(areaId, $event)"
|
|
8
13
|
>
|
|
9
14
|
<div
|
|
@@ -61,6 +66,12 @@
|
|
|
61
66
|
:disabled="field && field.readOnly"
|
|
62
67
|
:max-reached="maxReached"
|
|
63
68
|
:rendering="rendering(widget)"
|
|
69
|
+
:raised="raisedWidgets.has(widget._id)"
|
|
70
|
+
:style="{
|
|
71
|
+
'z-index': raisedWidgets.has(widget._id)
|
|
72
|
+
? next.length + 1
|
|
73
|
+
: null
|
|
74
|
+
}"
|
|
64
75
|
@up="up"
|
|
65
76
|
@down="down"
|
|
66
77
|
@remove="remove"
|
|
@@ -23,14 +23,13 @@
|
|
|
23
23
|
@keyup.enter="onKeyup"
|
|
24
24
|
>
|
|
25
25
|
<div
|
|
26
|
-
v-if="!breadcrumbDisabled"
|
|
27
26
|
ref="label"
|
|
28
27
|
class="apos-area-widget-controls apos-area-widget__label"
|
|
29
28
|
:class="labelsClasses"
|
|
30
29
|
>
|
|
31
30
|
<ol
|
|
32
31
|
class="apos-area-widget__breadcrumbs"
|
|
33
|
-
@click="
|
|
32
|
+
@click="clearSuppressionFlags"
|
|
34
33
|
>
|
|
35
34
|
<li
|
|
36
35
|
class="
|
|
@@ -92,7 +91,7 @@
|
|
|
92
91
|
/>
|
|
93
92
|
</div>
|
|
94
93
|
<div
|
|
95
|
-
v-if="!controlsDisabled"
|
|
94
|
+
v-if="!controlsDisabled && !maxReached && !isSuppressingAddContentButtons"
|
|
96
95
|
class="
|
|
97
96
|
apos-area-widget-controls
|
|
98
97
|
apos-area-widget-controls--add--top
|
|
@@ -143,43 +142,52 @@
|
|
|
143
142
|
@operation="onOperation"
|
|
144
143
|
/>
|
|
145
144
|
</div>
|
|
146
|
-
|
|
147
|
-
<!-- Still used for contextual editing components -->
|
|
148
|
-
<component
|
|
149
|
-
:is="widgetEditorComponent(widget.type)"
|
|
150
|
-
v-if="isContextual && !foreign"
|
|
151
|
-
:key="generation"
|
|
152
|
-
:class="adminContentDirectionClass"
|
|
153
|
-
:options="widgetOptions"
|
|
154
|
-
:type="widget.type"
|
|
155
|
-
:model-value="widget"
|
|
156
|
-
:meta="meta"
|
|
157
|
-
:doc-id="docId"
|
|
158
|
-
:focused="isFocused"
|
|
159
|
-
@update="$emit('update', $event)"
|
|
160
|
-
@suppress-widget-controls="isSuppressingWidgetControls = true"
|
|
161
|
-
/>
|
|
162
|
-
<component
|
|
163
|
-
:is="widgetComponent(widget.type)"
|
|
164
|
-
v-else
|
|
165
|
-
:id="widget._id"
|
|
166
|
-
:key="`${generation}-preview`"
|
|
167
|
-
:class="adminContentDirectionClass"
|
|
168
|
-
:options="widgetOptions"
|
|
169
|
-
:type="widget.type"
|
|
170
|
-
:area-field-id="fieldId"
|
|
171
|
-
:following-values="followingValuesWithParent"
|
|
172
|
-
:model-value="widget"
|
|
173
|
-
:value="widget"
|
|
174
|
-
:meta="meta"
|
|
175
|
-
:foreign="foreign"
|
|
176
|
-
:doc-id="docId"
|
|
177
|
-
:rendering="rendering"
|
|
178
|
-
@edit="$emit('edit', i);"
|
|
179
|
-
@update="$emit('update', $event);"
|
|
180
|
-
/>
|
|
181
145
|
<div
|
|
182
|
-
|
|
146
|
+
class="apos-area-widget-rendered-widget"
|
|
147
|
+
:style="{
|
|
148
|
+
'z-index': raised
|
|
149
|
+
? 0
|
|
150
|
+
: null
|
|
151
|
+
}"
|
|
152
|
+
>
|
|
153
|
+
<!-- Still used for contextual editing components -->
|
|
154
|
+
<component
|
|
155
|
+
:is="widgetEditorComponent(widget.type)"
|
|
156
|
+
v-if="isContextual && !foreign"
|
|
157
|
+
:key="generation"
|
|
158
|
+
:class="adminContentDirectionClass"
|
|
159
|
+
:options="widgetOptions"
|
|
160
|
+
:type="widget.type"
|
|
161
|
+
:model-value="widget"
|
|
162
|
+
:meta="meta"
|
|
163
|
+
:doc-id="docId"
|
|
164
|
+
:focused="isFocused"
|
|
165
|
+
@update="$emit('update', $event)"
|
|
166
|
+
@suppress-widget-controls="doSuppressWidgetControls"
|
|
167
|
+
@suppress-add-content-buttons="doSuppressAddContentButtons"
|
|
168
|
+
/>
|
|
169
|
+
<component
|
|
170
|
+
:is="widgetComponent(widget.type)"
|
|
171
|
+
v-else
|
|
172
|
+
:id="widget._id"
|
|
173
|
+
:key="`${generation}-preview`"
|
|
174
|
+
:class="adminContentDirectionClass"
|
|
175
|
+
:options="widgetOptions"
|
|
176
|
+
:type="widget.type"
|
|
177
|
+
:area-field-id="fieldId"
|
|
178
|
+
:following-values="followingValuesWithParent"
|
|
179
|
+
:model-value="widget"
|
|
180
|
+
:value="widget"
|
|
181
|
+
:meta="meta"
|
|
182
|
+
:foreign="foreign"
|
|
183
|
+
:doc-id="docId"
|
|
184
|
+
:rendering="rendering"
|
|
185
|
+
@edit="$emit('edit', i);"
|
|
186
|
+
@update="$emit('update', $event);"
|
|
187
|
+
/>
|
|
188
|
+
</div>
|
|
189
|
+
<div
|
|
190
|
+
v-if="!controlsDisabled && !maxReached && !isSuppressingAddContentButtons"
|
|
183
191
|
class="
|
|
184
192
|
apos-area-widget-controls
|
|
185
193
|
apos-area-widget-controls--add
|
|
@@ -209,13 +217,25 @@
|
|
|
209
217
|
|
|
210
218
|
<script>
|
|
211
219
|
import { mapState, mapActions } from 'pinia';
|
|
220
|
+
import { unref } from 'vue';
|
|
212
221
|
import { useWidgetStore } from 'Modules/@apostrophecms/ui/stores/widget';
|
|
213
222
|
import { useBreakpointPreviewStore } from 'Modules/@apostrophecms/ui/stores/breakpointPreview.js';
|
|
214
223
|
import { useModalStore } from 'Modules/@apostrophecms/ui/stores/modal.js';
|
|
224
|
+
import { useWidgetGraphStore } from 'Modules/@apostrophecms/ui/stores/widgetGraph.js';
|
|
215
225
|
|
|
216
226
|
export default {
|
|
217
227
|
name: 'AposAreaWidget',
|
|
228
|
+
inject: {
|
|
229
|
+
aposGraphKey: {
|
|
230
|
+
from: 'aposGraphKey',
|
|
231
|
+
default: null
|
|
232
|
+
}
|
|
233
|
+
},
|
|
218
234
|
props: {
|
|
235
|
+
raised: {
|
|
236
|
+
type: Boolean,
|
|
237
|
+
default: false
|
|
238
|
+
},
|
|
219
239
|
docId: {
|
|
220
240
|
type: String,
|
|
221
241
|
required: false,
|
|
@@ -316,6 +336,7 @@ export default {
|
|
|
316
336
|
mounted: false, // hack around needing DOM to be rendered for computed classes
|
|
317
337
|
menuOpen: null,
|
|
318
338
|
isSuppressingWidgetControls: false,
|
|
339
|
+
isSuppressingAddContentButtons: false,
|
|
319
340
|
hasClickOutsideListener: false,
|
|
320
341
|
classes: {
|
|
321
342
|
show: 'apos-is-visible',
|
|
@@ -447,7 +468,7 @@ export default {
|
|
|
447
468
|
},
|
|
448
469
|
labelsClasses() {
|
|
449
470
|
return {
|
|
450
|
-
[this.classes.show]: this.isHovered
|
|
471
|
+
[this.classes.show]: (this.isHovered && !this.focusedWidget) || this.isFocused,
|
|
451
472
|
// Force LTR for breadcrumbs for now so that nested AreaWidgets
|
|
452
473
|
// behave properly.
|
|
453
474
|
'apos-ltr': true
|
|
@@ -455,7 +476,7 @@ export default {
|
|
|
455
476
|
},
|
|
456
477
|
addClasses() {
|
|
457
478
|
return {
|
|
458
|
-
[this.classes.show]: this.isHovered || this.isFocused,
|
|
479
|
+
[this.classes.show]: (this.isHovered && !this.focusedWidget) || this.isFocused,
|
|
459
480
|
[`${this.classes.open}--menu-${this.menuOpen}`]: !!this.menuOpen
|
|
460
481
|
};
|
|
461
482
|
},
|
|
@@ -493,6 +514,7 @@ export default {
|
|
|
493
514
|
} else {
|
|
494
515
|
this.menuOpen = null;
|
|
495
516
|
this.isSuppressingWidgetControls = false;
|
|
517
|
+
this.isSuppressingAddContentButtons = false;
|
|
496
518
|
this.removeClickOutsideListener();
|
|
497
519
|
}
|
|
498
520
|
// Helps get scroll tracking unstuck on new/modified widgets
|
|
@@ -515,11 +537,14 @@ export default {
|
|
|
515
537
|
// a 'focus my parent' plea
|
|
516
538
|
apos.bus.$on('widget-focus-parent', this.focusParent);
|
|
517
539
|
apos.bus.$on('context-menu-toggled', this.getFocusForMenu);
|
|
540
|
+
apos.bus.$on('suppress-focused-widget-controls', this.doSuppressWidgetControls);
|
|
518
541
|
|
|
519
542
|
this.breadcrumbs.$lastEl = this.$el;
|
|
520
543
|
|
|
521
544
|
this.getBreadcrumbs();
|
|
522
545
|
|
|
546
|
+
this.registerInGraph();
|
|
547
|
+
|
|
523
548
|
if (this.focusedWidget) {
|
|
524
549
|
// If another widget was in focus (because the user clicked the "add"
|
|
525
550
|
// menu, for example), and this widget was created, give the new widget
|
|
@@ -549,12 +574,50 @@ export default {
|
|
|
549
574
|
unmounted() {
|
|
550
575
|
// Remove the focus parent listener when unmounted
|
|
551
576
|
apos.bus.$off('widget-focus-parent', this.focusParent);
|
|
577
|
+
apos.bus.$off('suppress-focused-widget-controls', this.doSuppressWidgetControls);
|
|
552
578
|
window.removeEventListener('scroll', this.stickyControlsScroll);
|
|
553
579
|
window.removeEventListener('resize', this.stickyControlsResize);
|
|
580
|
+
this.unregisterFromGraph();
|
|
554
581
|
},
|
|
555
582
|
methods: {
|
|
583
|
+
clearSuppressionFlags() {
|
|
584
|
+
this.isSuppressingWidgetControls = false;
|
|
585
|
+
this.isSuppressingAddContentButtons = false;
|
|
586
|
+
},
|
|
556
587
|
...mapActions(useWidgetStore, [ 'setFocusedWidget', 'setHoveredWidget' ]),
|
|
557
588
|
...mapActions(useModalStore, [ 'getAdminContentDirectionClass' ]),
|
|
589
|
+
...mapActions(useWidgetGraphStore, {
|
|
590
|
+
storeRegisterWidget: 'registerWidget',
|
|
591
|
+
storeUnregisterWidget: 'unregisterWidget'
|
|
592
|
+
}),
|
|
593
|
+
registerInGraph() {
|
|
594
|
+
if (this.foreign) {
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
const graphKey = unref(this.aposGraphKey);
|
|
598
|
+
if (!graphKey) {
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
this.storeRegisterWidget(graphKey, this.widget, {
|
|
602
|
+
areaId: this.areaId
|
|
603
|
+
});
|
|
604
|
+
},
|
|
605
|
+
unregisterFromGraph() {
|
|
606
|
+
if (this.foreign) {
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
const graphKey = unref(this.aposGraphKey);
|
|
610
|
+
if (!graphKey) {
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
this.storeUnregisterWidget(graphKey, this.widget._id);
|
|
614
|
+
},
|
|
615
|
+
doSuppressWidgetControls() {
|
|
616
|
+
this.isSuppressingWidgetControls = true;
|
|
617
|
+
},
|
|
618
|
+
doSuppressAddContentButtons() {
|
|
619
|
+
this.isSuppressingAddContentButtons = true;
|
|
620
|
+
},
|
|
558
621
|
// Emits same actions as the native operations,
|
|
559
622
|
// e.g ('edit', { index }), ('remove', { index }), etc.
|
|
560
623
|
onOperation({ name, payload }) {
|
|
@@ -716,6 +779,9 @@ export default {
|
|
|
716
779
|
}
|
|
717
780
|
},
|
|
718
781
|
unfocus(event) {
|
|
782
|
+
if (event.target.closest('[data-apos-ignore-unfocus="true"]')) {
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
719
785
|
if (!this.$el.contains(event.target)) {
|
|
720
786
|
this.removeClickOutsideListener();
|
|
721
787
|
|
|
@@ -925,6 +991,10 @@ export default {
|
|
|
925
991
|
}
|
|
926
992
|
}
|
|
927
993
|
|
|
994
|
+
.apos-area-widget-rendered-widget {
|
|
995
|
+
position: relative;
|
|
996
|
+
}
|
|
997
|
+
|
|
928
998
|
.apos-area-widget-controls {
|
|
929
999
|
z-index: $z-index-widget-controls;
|
|
930
1000
|
position: absolute;
|