@simmalugnt-se/payload-visual-editing 0.3.1 → 0.3.3

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/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.3
4
+
5
+ - Focusing a field in Admin outlines a copy the preview shows, when the page renders a row more
6
+ than once (a desktop and a mobile menu): rendered copies win over hidden ones, and a copy with the
7
+ field over one with only its row. The outline used to go to the first copy in the page, which a
8
+ narrow preview might hide.
9
+
10
+ ## 0.3.2
11
+
12
+ - npm links to simmalugnt.se instead of the private source repository.
13
+
3
14
  ## 0.3.1
4
15
 
5
16
  - Preview clicks open collapsed Payload `collapsible` fields around the target, including nested
@@ -2,6 +2,7 @@
2
2
  import { useEffect } from "react";
3
3
  import { ATTR_BLOCK, ATTR_BLOCK_NAME, ATTR_BLOCK_TYPE, ATTR_FIELD, ATTR_GLOBAL, ATTR_RICHTEXT, inScope, isFocusMessage, isLabelsMessage, MESSAGE_TYPE, nodeSnippet, } from "../protocol.js";
4
4
  import { HIGHLIGHT_COLOR, HIGHLIGHT_TINT } from "../theme.js";
5
+ import { preferredCandidate } from "./preferred.js";
5
6
  const SELECTOR = `[${ATTR_FIELD}], [${ATTR_BLOCK}]`;
6
7
  /**
7
8
  * Render only in draft mode. Inside the Live Preview window it outlines editable blocks
@@ -220,21 +221,31 @@ function hitForBlock(blockId, field, labels) {
220
221
  if (!blockId) {
221
222
  // A top-level field: marked outside any block of this document.
222
223
  const element = field
223
- ? Array.from(document.querySelectorAll(`[${ATTR_FIELD}="${CSS.escape(field)}"]`)).find((candidate) => ownerOf(candidate, labels)?.block === null)
224
+ ? preferredCandidate(Array.from(document.querySelectorAll(`[${ATTR_FIELD}="${CSS.escape(field)}"]`))
225
+ .filter((candidate) => ownerOf(candidate, labels)?.block === null)
226
+ .map((candidate) => ({
227
+ item: candidate,
228
+ rendered: isRendered(candidate),
229
+ isField: true,
230
+ })))
224
231
  : undefined;
225
232
  return element && field ? { element, field, label: labelFor(null, field, labels) } : null;
226
233
  }
227
- const block = document.querySelector(`[${ATTR_BLOCK}="${CSS.escape(blockId)}"]`);
228
- if (!block || !ownerOf(block, labels)) {
229
- return null;
230
- }
231
- const fieldElement = field
232
- ? Array.from(block.querySelectorAll(`[${ATTR_FIELD}="${CSS.escape(field)}"]`)).find((element) => element.closest(`[${ATTR_BLOCK}]`) === block)
233
- : undefined;
234
- if (field && fieldElement) {
235
- return { element: fieldElement, blockId, field, label: labelFor(block, field, labels) };
236
- }
237
- return { element: block, blockId, label: labelFor(block, undefined, labels) };
234
+ // The same row may be rendered more than once, e.g. in a desktop and a mobile menu.
235
+ const blocks = Array.from(document.querySelectorAll(`[${ATTR_BLOCK}="${CSS.escape(blockId)}"]`)).filter((block) => ownerOf(block, labels));
236
+ return (preferredCandidate(blocks.map((block) => {
237
+ const fieldElement = field
238
+ ? Array.from(block.querySelectorAll(`[${ATTR_FIELD}="${CSS.escape(field)}"]`)).find((element) => element.closest(`[${ATTR_BLOCK}]`) === block)
239
+ : undefined;
240
+ const hit = field && fieldElement
241
+ ? { element: fieldElement, blockId, field, label: labelFor(block, field, labels) }
242
+ : { element: block, blockId, label: labelFor(block, undefined, labels) };
243
+ return { item: hit, rendered: isRendered(hit.element), isField: hit.field !== undefined };
244
+ })) ?? null);
245
+ }
246
+ /** Laid out on the page: an element inside `display: none` has no boxes. */
247
+ function isRendered(element) {
248
+ return element.getClientRects().length > 0;
238
249
  }
239
250
  /**
240
251
  * Same order as Admin's row header: the editor's block name, then the block's label, then its slug.
@@ -0,0 +1,13 @@
1
+ /** One place a row or field is rendered. Pure, so the choice can be tested without a DOM. */
2
+ export type Candidate<T> = {
3
+ item: T;
4
+ /** Laid out on the page; false inside `display: none`, e.g. a desktop menu on a phone. */
5
+ rendered: boolean;
6
+ /** The field itself, not only its row. */
7
+ isField: boolean;
8
+ };
9
+ /**
10
+ * Pages render some rows more than once (a desktop and a mobile menu, a list and its images).
11
+ * Prefer a rendered field, then a rendered row, then a hidden field; on a tie the first in the DOM.
12
+ */
13
+ export declare function preferredCandidate<T>(candidates: Candidate<T>[]): T | undefined;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Pages render some rows more than once (a desktop and a mobile menu, a list and its images).
3
+ * Prefer a rendered field, then a rendered row, then a hidden field; on a tie the first in the DOM.
4
+ */
5
+ export function preferredCandidate(candidates) {
6
+ let best;
7
+ for (const candidate of candidates) {
8
+ if (!best || rank(candidate) > rank(best)) {
9
+ best = candidate;
10
+ }
11
+ }
12
+ return best?.item;
13
+ }
14
+ function rank(candidate) {
15
+ return (candidate.rendered ? 2 : 0) + (candidate.isField ? 1 : 0);
16
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simmalugnt-se/payload-visual-editing",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "Click a block in Payload Live Preview to open its fields in the edit form",
5
5
  "keywords": [
6
6
  "payload",
@@ -8,11 +8,7 @@
8
8
  ],
9
9
  "license": "MIT",
10
10
  "author": "Simmalugnt AB",
11
- "repository": {
12
- "type": "git",
13
- "url": "git+https://github.com/simmalugnt-se/payload-plugins.git",
14
- "directory": "packages/payload-visual-editing"
15
- },
11
+ "homepage": "https://simmalugnt.se",
16
12
  "type": "module",
17
13
  "engines": {
18
14
  "node": ">=22"