@panphora/hyper-cms 0.11.3 → 0.11.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@panphora/hyper-cms",
3
- "version": "0.11.3",
3
+ "version": "0.11.4",
4
4
  "description": "Live edit-in-place CMS sidebar for HTML pages with a hyper-html-api rules tag",
5
5
  "keywords": [
6
6
  "HTML",
@@ -47,8 +47,8 @@
47
47
  "test:all": "npm run test:node && npm run test:browser"
48
48
  },
49
49
  "dependencies": {
50
- "hyper-html-api": "^0.10.0",
51
- "hyper-morph": "^1.3.0"
50
+ "hyper-html-api": "^0.10.1",
51
+ "hyper-morph": "^1.6.2"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@open-wc/testing": "^4.0.0",
@@ -1,6 +1,6 @@
1
1
  // GENERATED from clayjs/src/lib/content-dom.js. Edit ClayJS and run `npm run propagate:foundation`.
2
2
 
3
- import { capabilitySelector, hasCapability } from './region-capabilities.js'
3
+ import { capabilitySelector, hasCapability, CAPABILITIES, expandsTo, hasPolicyToken } from './region-capabilities.js'
4
4
 
5
5
  const UNSUPPORTED_SELECTOR = /:(?:focus(?:-within|-visible)?|hover|active|visited|defined)\b/i
6
6
 
@@ -9,10 +9,9 @@ function copyControlState(source, copy) {
9
9
  const type = (source.getAttribute?.('type') || '').toLowerCase()
10
10
  if ('value' in source && 'value' in copy && source.tagName !== 'OPTION' && type !== 'checkbox' && type !== 'radio' && type !== 'file') copy.value = source.value
11
11
  if (source.tagName === 'INPUT' && (type === 'checkbox' || type === 'radio')) copy.checked = source.checked
12
+ // Each surviving option carries its own selection, so a <select> whose excluded
13
+ // options were removed from the copy keeps the rest in step with the page.
12
14
  if (source.tagName === 'OPTION') copy.selected = source.selected
13
- if (source.tagName === 'SELECT') {
14
- for (let i = 0; i < source.options.length; i++) copy.options[i].selected = source.options[i].selected
15
- }
16
15
  if ('indeterminate' in source && 'indeterminate' in copy) copy.indeterminate = source.indeterminate
17
16
  }
18
17
 
@@ -27,46 +26,77 @@ function matchesWithin(node, selector, boundary, inherit) {
27
26
  return false
28
27
  }
29
28
 
30
- function importTree(source, targetDocument, capability, capabilityMatch, exclude, boundary, inherit, maps) {
31
- if (source.nodeType === 1 && (
32
- (inherit
33
- ? hasCapability(source, capability)
34
- : matchesWithin(source, capabilityMatch, boundary, false)) ||
35
- (exclude && (inherit && source !== boundary ? source.matches(exclude) : matchesWithin(source, exclude, boundary, inherit)))
36
- )) return null
37
- const copy = targetDocument.importNode(source, false)
29
+ // One native deep clone, then a lockstep walk of the two isomorphic trees that
30
+ // records provenance, collects controls, and marks excluded subtrees for removal.
31
+ // The browser's clone is far cheaper than importing node by node. The root answers
32
+ // for its ancestors once; below it the walk is top-down, so with inheritance each
33
+ // element answers only for its own markers: an excluded ancestor is never entered.
34
+ function excluded(source, capability, capabilityMatch, exclude, boundary, inherit) {
35
+ return (inherit
36
+ ? hasCapability(source, capability)
37
+ : matchesWithin(source, capabilityMatch, boundary, false)) ||
38
+ !!(exclude && (inherit && source !== boundary ? source.matches(exclude) : matchesWithin(source, exclude, boundary, inherit)))
39
+ }
40
+
41
+ // The test closestWithCapability makes at each level, for this element alone.
42
+ function hasOwnCapability(element, capability) {
43
+ const definition = CAPABILITIES[capability]
44
+ return definition.tokens.some(token => expandsTo(element, token)) ||
45
+ definition.bundles.some(bundle => hasPolicyToken(element, bundle))
46
+ }
47
+
48
+ const CONTROL_TAGS = new Set(['INPUT', 'TEXTAREA', 'SELECT', 'OPTION'])
49
+
50
+ function pairTree(source, copy, maps, drop) {
51
+ if (source.nodeType === 1 && drop(source)) { maps.dropped.push(copy); return }
38
52
  maps.cloneToLive.set(copy, source)
39
53
  maps.liveToClone.set(source, copy)
40
- const sourceChildren = source.nodeType === 1 && source.tagName === 'TEMPLATE' ? source.content : source
41
- const copyChildren = copy.nodeType === 1 && copy.tagName === 'TEMPLATE' ? copy.content : copy
42
- if (sourceChildren !== source) {
43
- maps.cloneToLive.set(copyChildren, sourceChildren)
44
- maps.liveToClone.set(sourceChildren, copyChildren)
45
- }
46
- for (const child of sourceChildren.childNodes || []) {
47
- const childCopy = importTree(child, targetDocument, capability, capabilityMatch, exclude, boundary, inherit, maps)
48
- if (childCopy) {
49
- copyChildren.appendChild(childCopy)
50
- if (child.nodeType === 1) copyControlState(child, childCopy)
54
+ let sourceChildren = source
55
+ let copyChildren = copy
56
+ if (source.nodeType === 1) {
57
+ if (CONTROL_TAGS.has(source.tagName)) maps.controls.push(source, copy)
58
+ if (source.tagName === 'TEMPLATE') {
59
+ // The deep clone also copied the template's own child nodes, which are not its
60
+ // content and were never part of the view.
61
+ while (copy.firstChild) copy.removeChild(copy.firstChild)
62
+ sourceChildren = source.content
63
+ copyChildren = copy.content
64
+ maps.cloneToLive.set(copyChildren, sourceChildren)
65
+ maps.liveToClone.set(sourceChildren, copyChildren)
51
66
  }
67
+ } else if (source.nodeType !== 11) return
68
+ let c = copyChildren.firstChild
69
+ for (let s = sourceChildren.firstChild; s && c; s = s.nextSibling) {
70
+ pairTree(s, c, maps, drop)
71
+ c = c.nextSibling
52
72
  }
53
- if (source.nodeType === 1) copyControlState(source, copy)
54
- return copy
55
73
  }
56
74
 
57
- export function createContentView(context, { capability = 'data', exclude = null, inherit = true } = {}) {
75
+ export function createContentView(context, { capability = 'data', exclude = null, inherit = true, provenance = null } = {}) {
58
76
  if (!context) throw new TypeError('createContentView requires a DOM context')
59
77
  const sourceDocument = context.nodeType === 9 ? context : context.ownerDocument
60
78
  if (!sourceDocument?.implementation?.createHTMLDocument) {
61
79
  throw new TypeError('createContentView requires an HTML DOM implementation')
62
80
  }
63
81
  const inertDocument = sourceDocument.implementation.createHTMLDocument('')
64
- const cloneToLive = new WeakMap()
82
+ const cloneToLive = provenance || new WeakMap()
65
83
  const liveToClone = new WeakMap()
66
84
  const sourceRoot = context.nodeType === 9 ? context.documentElement : context
67
85
  if (exclude) inertDocument.documentElement.matches(exclude)
68
86
  const capabilityMatch = capabilitySelector(capability)
69
- const root = importTree(sourceRoot, inertDocument, capability, capabilityMatch, exclude, sourceRoot, inherit, { cloneToLive, liveToClone })
87
+ const drop = inherit
88
+ ? (el) => hasOwnCapability(el, capability) || !!(exclude && el.matches(exclude))
89
+ : (el) => excluded(el, capability, capabilityMatch, exclude, sourceRoot, false)
90
+ let root = null
91
+ if (sourceRoot.nodeType !== 1 || !excluded(sourceRoot, capability, capabilityMatch, exclude, sourceRoot, inherit)) {
92
+ root = inertDocument.importNode(sourceRoot, true)
93
+ const maps = { cloneToLive, liveToClone, dropped: [], controls: [] }
94
+ pairTree(sourceRoot, root, maps, drop)
95
+ for (const node of maps.dropped) node.remove()
96
+ // Control state is copied once the copy holds only what the view keeps, so a
97
+ // <select> sees exactly the options the old node-by-node import gave it.
98
+ for (let i = 0; i < maps.controls.length; i += 2) copyControlState(maps.controls[i], maps.controls[i + 1])
99
+ }
70
100
  if (context.nodeType === 9 && root) {
71
101
  inertDocument.replaceChild(root, inertDocument.documentElement)
72
102
  liveToClone.set(context, inertDocument)
@@ -23,6 +23,7 @@ export const POLICY_TOKENS = Object.freeze([
23
23
  'no-data',
24
24
  'freeze',
25
25
  'editor-ui',
26
+ 'live',
26
27
  ])
27
28
 
28
29
  export function hasPolicyToken(element, token) {