PluginProbe
Extendify / 3.2.1
Extendify v3.2.1
3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 0.6.0 All 127 releases
extendify / src / Agent / lib / block-el.js

block-el.js in Extendify 3.2.1, at src/Agent/lib/block-el.js

74 lines 2.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 // Post content and template parts each have their own tagger and id space, so
2 // a rendered block answers to whichever attribute stamped it.
3 const ID_ATTRS = [
4 'data-extendify-agent-block-id',
5 'data-extendify-part-block-id',
6 ];
7
8 export const BLOCK_ID_SEL = ID_ATTRS.map((attr) => `[${attr}]`).join(', ');
9
10 export const idAttrOf = (el) =>
11 ID_ATTRS.find((attr) => el?.getAttribute?.(attr) != null) ?? ID_ATTRS[0];
12
13 export const blockIdOf = (el) => {
14 const attr = ID_ATTRS.find((a) => el?.getAttribute?.(a));
15 return attr ? el.getAttribute(attr) : null;
16 };
17
18 // A nav's hidden overlay copy comes first, so an unfiltered match is invisible.
19 const isRendered = (el) => {
20 for (let node = el; node; node = node.parentElement) {
21 const style = node.ownerDocument?.defaultView?.getComputedStyle?.(node);
22 if (!style) return true;
23 if (style.display === 'none' || style.visibility === 'hidden') return false;
24 }
25 return true;
26 };
27
28 const PART_SLUG_ATTR = 'data-extendify-part-slug';
29
30 const slugOf = (el) =>
31 el?.closest?.(`[${PART_SLUG_ATTR}]`)?.getAttribute(PART_SLUG_ATTR) ?? null;
32
33 export const scopeOf = (block) => ({
34 partSlug: block?.source?.partSlug || null,
35 });
36
37 // Every part numbers from 1, so id 3 exists in the page, header and footer.
38 const SCOPED_ID_RX = /^part:([^:]+):(.+)$/;
39
40 export const scopedBlockId = (blockId, partSlug) =>
41 partSlug ? `part:${partSlug}:${blockId}` : String(blockId);
42
43 export const parseScopedId = (blockId) => {
44 const match = SCOPED_ID_RX.exec(String(blockId ?? ''));
45 return match
46 ? { partSlug: match[1], blockId: match[2] }
47 : { partSlug: null, blockId: String(blockId ?? '') };
48 };
49
50 // An unqualified id means post content; unscoped it would match the header's.
51 export const resolveScopedId = (scopedId, root = document) => {
52 const { partSlug, blockId } = parseScopedId(scopedId);
53 return findBlockEl(blockId, root, { partSlug });
54 };
55
56 // Parts render first and every space numbers from 1, so an unscoped id hits
57 // the header.
58 export const findBlockEl = (blockId, root = document, scope = null) => {
59 const matches = [
60 ...root.querySelectorAll(
61 ID_ATTRS.map((attr) => `[${attr}="${CSS.escape(String(blockId))}"]`).join(
62 ', ',
63 ),
64 ),
65 ];
66 if (matches.length < 2) return matches[0] ?? null;
67 const inScope = scope
68 ? matches.filter((el) => slugOf(el) === (scope.partSlug || null))
69 : [];
70 // A pattern used in both a part and post content may only render outside scope.
71 const pool = inScope.length ? inScope : matches;
72 return pool.find(isRendered) ?? pool[0];
73 };
74