akari-video 1.3.0-beta.1 → 1.3.0-beta.2

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": "akari-video",
3
- "version": "1.3.0-beta.1",
3
+ "version": "1.3.0-beta.2",
4
4
  "description": "AKARI Video launcher CLI — start an AI-edited video project from any directory: scaffold, connection check, then hand over to Claude Code (or opencode). AKARI Video を opencode や Claude Code で、どのディレクトリからでも始めるための `akari` ランチャー CLI。接続確認(doctor)→ 未セットアップならプロジェクト雛形を作成 → AI エージェントを起動する。外部 npm 依存ゼロ(Node.js 組み込みモジュールのみ)。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akari-video",
3
- "version": "1.3.0-beta.1",
3
+ "version": "1.3.0-beta.2",
4
4
  "description": "AKARI Video launcher CLI — start an AI-edited video project from any directory: scaffold, connection check, then hand over to Claude Code (or opencode). AKARI Video を opencode や Claude Code で、どのディレクトリからでも始めるための `akari` ランチャー CLI。接続確認(doctor)→ 未セットアップならプロジェクト雛形を作成 → AI エージェントを起動する。外部 npm 依存ゼロ(Node.js 組み込みモジュールのみ)。 [akari-video npm vendor: bin/akari.mjs is reference-only. These CLI entrypoints are not included in the akari-video npm package. Use `akari doctor --json` and run the path reported in `render_cut.path`. Full installations provide it in a monorepo checkout, ~/.akari/app, /Applications/AKARI Video.app/Contents/Resources/packages, or %LOCALAPPDATA%\\Programs\\@akari-videoshell\\resources\\packages.]",
5
5
  "type": "module",
6
6
  "files": [
@@ -5,7 +5,7 @@ import { mkdtemp, rm, writeFile, chmod } from "node:fs/promises";
5
5
  import { syncBuiltinESMExports } from "node:module";
6
6
  import { tmpdir } from "node:os";
7
7
  import path from "node:path";
8
- import test from "node:test";
8
+ import test, { after } from "node:test";
9
9
  import { fileURLToPath } from "node:url";
10
10
 
11
11
  import { resolveFfmpeg, resolveFfprobe } from "../src/index.mjs";
@@ -37,9 +37,10 @@ function baseEnv(overrides = {}) {
37
37
  };
38
38
  }
39
39
 
40
- // PATH からシステムの ffmpeg/ffprobe を外し、vendor/ 同梱バイナリへの
41
- // フォールバックだけが効く状態を作る(task.md 記載の `env PATH=/usr/bin:/bin` と同じ狙い)。
42
- const STRIPPED_PATH_ENV = baseEnv({ PATH: "/usr/bin:/bin" });
40
+ // PATH からシステムの ffmpeg/ffprobe を OS に依らず外す。
41
+ const emptyPathDirectory = fs.mkdtempSync(path.join(tmpdir(), 'akari-media-bin-empty-path-'));
42
+ after(() => fs.rmSync(emptyPathDirectory, { recursive: true, force: true }));
43
+ const STRIPPED_PATH_ENV = baseEnv({ PATH: emptyPathDirectory });
43
44
 
44
45
  test("packagedBinaryPath: packageRoot の ../../media-bin を候補にし、win32 は .exe を付ける", () => {
45
46
  const packageRoot = fileURLToPath(new URL("../", import.meta.url));
@@ -192,7 +193,8 @@ test("resolveFfmpeg: PATH に ffmpeg があればコマンド名を返す", () =
192
193
  assert.equal(resolved, "ffmpeg");
193
194
  });
194
195
 
195
- test("resolveFfmpeg: PATH から外すと vendor 同梱バイナリへフォールバックし、実行できる", () => {
196
+ test("resolveFfmpeg: PATH から外すと vendor 同梱バイナリへフォールバックし、実行できる", t => {
197
+ if (!fs.existsSync(vendorBinaryPath('ffmpeg'))) return t.skip('vendor ffmpeg is not installed when postinstall is skipped');
196
198
  const resolved = resolveFfmpeg({ env: STRIPPED_PATH_ENV });
197
199
  assert.notEqual(resolved, "ffmpeg");
198
200
  assert.ok(path.isAbsolute(resolved), `絶対パスのはず: ${resolved}`);
@@ -202,7 +204,8 @@ test("resolveFfmpeg: PATH から外すと vendor 同梱バイナリへフォー
202
204
  assert.match(result.stdout, /ffmpeg version/);
203
205
  });
204
206
 
205
- test("resolveFfprobe: PATH から外すと vendor 同梱バイナリへフォールバックし、実行できる", () => {
207
+ test("resolveFfprobe: PATH から外すと vendor 同梱バイナリへフォールバックし、実行できる", t => {
208
+ if (!fs.existsSync(vendorBinaryPath('ffprobe'))) return t.skip('vendor ffprobe is not installed when postinstall is skipped');
206
209
  const resolved = resolveFfprobe({ env: STRIPPED_PATH_ENV });
207
210
  assert.notEqual(resolved, "ffprobe");
208
211
  assert.ok(path.isAbsolute(resolved), `絶対パスのはず: ${resolved}`);
@@ -100,7 +100,7 @@ test('サイドカー生成中もイベントループは止まらない(spawn
100
100
  assert.equal(result.ok, true, result.reason);
101
101
  assert.equal(result.skipped, false);
102
102
  assert.deepEqual(Object.keys(result).sort(),
103
- ['channels', 'durationSec', 'key', 'ok', 'path', 'reason', 'sampleRate', 'skipped']);
103
+ ['channels', 'durationSec', 'format', 'key', 'ok', 'path', 'reason', 'sampleRate', 'skipped']);
104
104
  assert.equal(result.sampleRate, 48000);
105
105
  assert.equal(result.channels, 1);
106
106
  assert.equal(result.durationSec, 1);
@@ -1,6 +1,16 @@
1
1
  export declare function elementAddress(root: Element, element: Element): string | null;
2
- export declare function selectableElement(root: Element, element: Element, outputRect?: DOMRect): boolean;
3
- export declare function nearestSelectableElement(root: Element, hit: Element, outputRect?: DOMRect): Element | null;
4
- export declare function firstSelectableElement(root: Element, outputRect?: DOMRect): Element | null;
2
+ export interface SelectionBounds { left: number; top: number; right: number; bottom: number; width: number; height: number }
3
+ export declare function drawsSelectionContent(element: Element, style?: CSSStyleDeclaration): boolean;
4
+ export declare function selectionContentBounds(element: Element, options?: {
5
+ container?: Element;
6
+ visibleRect?: (rect: DOMRect, element: Element) => SelectionBounds | null;
7
+ rawRect?: (element: Element) => DOMRect;
8
+ }): SelectionBounds | null;
9
+ export declare function isSelectionContainer(element: Element): boolean;
10
+ export declare function selectableContainer(root: Element, element: Element, outputRect: DOMRect | undefined, bounds: SelectionBounds | null, options?: SelectionOptions): boolean;
11
+ export interface SelectionOptions { boundsFor?: (element: Element) => SelectionBounds | null }
12
+ export declare function selectableElement(root: Element, element: Element, outputRect?: DOMRect, options?: SelectionOptions): boolean;
13
+ export declare function nearestSelectableElement(root: Element, hit: Element, outputRect?: DOMRect, options?: SelectionOptions): Element | null;
14
+ export declare function firstSelectableElement(root: Element, outputRect?: DOMRect, options?: SelectionOptions): Element | null;
5
15
  export declare function elementByAddress(root: Element, ref: string): Element | null;
6
16
  export declare function elementLabel(root: Element, element: Element): string;
@@ -17,30 +17,163 @@ function elementAddress(root, element) {
17
17
  return index < 0 ? null : `${id ? '#' : '.'}${id || token}[${index}]`;
18
18
  }
19
19
 
20
- function selectableElement(root, element, outputRect) {
20
+ function drawsSelectionContent(element, style = getComputedStyle(element)) {
21
+ const tag = element.tagName.toUpperCase();
22
+ if (['BASE', 'HEAD', 'LINK', 'META', 'NOSCRIPT', 'SCRIPT', 'STYLE', 'TEMPLATE', 'TITLE'].includes(tag)) return false;
23
+ if (['AUDIO', 'CANVAS', 'EMBED', 'IFRAME', 'IMG', 'OBJECT', 'SVG', 'VIDEO'].includes(tag)) return true;
24
+ if (element.namespaceURI === 'http://www.w3.org/2000/svg'
25
+ && ['path', 'rect', 'circle', 'ellipse', 'line', 'polyline', 'polygon', 'use', 'text', 'image'].includes(tag.toLowerCase())) return true;
26
+ if ([...element.childNodes].some(node => node.nodeType === 3 && node.textContent.trim())) return true;
27
+ const transparent = value => {
28
+ const color = String(value ?? '').trim().toLowerCase();
29
+ if (!color || color === 'transparent') return true;
30
+ const legacy = color.match(/^(?:rgba|hsla)\([^)]*,\s*([\d.]+)\)$/);
31
+ if (legacy) return Number(legacy[1]) <= 0;
32
+ const modern = color.match(/\/\s*([\d.]+)%?\s*\)$/);
33
+ return Boolean(modern && Number(modern[1]) <= 0);
34
+ };
35
+ const shadow = String(style.boxShadow ?? '').trim().toLowerCase();
36
+ const shadowColors = shadow.match(/(?:rgba?|hsla?|color)\([^)]*\)|transparent/g) ?? [];
37
+ if (!transparent(style.backgroundColor) || style.backgroundImage && style.backgroundImage !== 'none'
38
+ || shadow && shadow !== 'none' && (shadowColors.length === 0 || shadowColors.some(color => !transparent(color)))) return true;
39
+ for (const side of ['Top', 'Right', 'Bottom', 'Left']) {
40
+ if (parseFloat(style[`border${side}Width`]) > 0
41
+ && !['none', 'hidden'].includes(style[`border${side}Style`])
42
+ && !transparent(style[`border${side}Color`])) return true;
43
+ }
44
+ return parseFloat(style.outlineWidth) > 0 && !['none', 'hidden'].includes(style.outlineStyle)
45
+ && !transparent(style.outlineColor);
46
+ }
47
+
48
+ function selectionVisibleRect(rect, element, container) {
49
+ if (!rect || !(rect.width > 0 && rect.height > 0)) return null;
50
+ let { left, top, right, bottom } = rect;
51
+ for (let node = element; node && node !== container; node = node.parentElement) {
52
+ const style = getComputedStyle(node);
53
+ const paint = /\b(?:paint|content|strict)\b/.test(style.contain ?? '');
54
+ const clipX = paint || node.tagName.toLowerCase() === 'svg' || (style.overflowX || style.overflow) !== 'visible';
55
+ const clipY = paint || node.tagName.toLowerCase() === 'svg' || (style.overflowY || style.overflow) !== 'visible';
56
+ if (clipX || clipY) {
57
+ const clip = node.getBoundingClientRect();
58
+ if (clipX) { left = Math.max(left, clip.left); right = Math.min(right, clip.right); }
59
+ if (clipY) { top = Math.max(top, clip.top); bottom = Math.min(bottom, clip.bottom); }
60
+ }
61
+ if (right <= left || bottom <= top) return null;
62
+ }
63
+ return { left, top, right, bottom, width: right - left, height: bottom - top };
64
+ }
65
+
66
+ function selectionContentBounds(element, options = {}) {
67
+ if (!element) return null;
68
+ let left = Infinity, top = Infinity, right = -Infinity, bottom = -Infinity;
69
+ for (const node of [element, ...element.querySelectorAll('*')]) {
70
+ if (node.closest('[data-akari-interaction]') || node.closest('defs, symbol, pattern, clipPath, mask, marker')) continue;
71
+ let visible = true;
72
+ for (let ancestor = node; ancestor && ancestor !== options.container; ancestor = ancestor.parentElement) {
73
+ const style = getComputedStyle(ancestor);
74
+ if (style.display === 'none' || ['hidden', 'collapse'].includes(style.visibility)
75
+ || Number(style.opacity) === 0) { visible = false; break; }
76
+ }
77
+ if (!visible || !drawsSelectionContent(node)) continue;
78
+ if (node.tagName.toLowerCase() === 'svg'
79
+ && node.querySelector('path, rect, circle, ellipse, line, polyline, polygon, use, text, image')
80
+ && getComputedStyle(node).backgroundColor === 'rgba(0, 0, 0, 0)') continue;
81
+ const raw = options.rawRect?.(node) ?? node.getBoundingClientRect();
82
+ const rect = options.visibleRect ? options.visibleRect(raw, node)
83
+ : selectionVisibleRect(raw, node, options.container);
84
+ if (!rect || !(rect.width > 0 && rect.height > 0)) continue;
85
+ left = Math.min(left, rect.left); top = Math.min(top, rect.top);
86
+ right = Math.max(right, rect.right); bottom = Math.max(bottom, rect.bottom);
87
+ }
88
+ return Number.isFinite(left) ? { left, top, right, bottom, width: right - left, height: bottom - top } : null;
89
+ }
90
+
91
+ function selectableContentBranches(root, element, outputRect) {
92
+ let count = 0;
93
+ const visit = node => {
94
+ if (count > 1) return;
95
+ const style = getComputedStyle(node);
96
+ if (style.display === 'none' || ['hidden', 'collapse'].includes(style.visibility)
97
+ || Number(style.opacity) === 0 || isRuntimeElement(node)) return;
98
+ const rect = node.getBoundingClientRect();
99
+ if (node !== element && elementAddress(root, node) && rect.width > 1 && rect.height > 1
100
+ && drawsSelectionContent(node) && (!outputRect || rect.width < outputRect.width * 0.95
101
+ || rect.height < outputRect.height * 0.95)) { count++; return; }
102
+ for (const child of node.children) visit(child);
103
+ };
104
+ for (const child of element.children) visit(child);
105
+ return count;
106
+ }
107
+
108
+ function isSelectionContainer(element) {
109
+ return Boolean(element?.children?.length) && !drawsSelectionContent(element);
110
+ }
111
+
112
+ function selectionBoundsFor(element, options) {
113
+ const memo = options.boundsMemo ??= new Map();
114
+ if (!memo.has(element)) memo.set(element,
115
+ options.boundsFor ? options.boundsFor(element) : selectionContentBounds(element));
116
+ return memo.get(element);
117
+ }
118
+
119
+ function hasUncoveredPaintedContent(root, element, outputRect, options) {
120
+ const visit = node => {
121
+ if (node.closest('[data-akari-interaction], defs, symbol, pattern, clipPath, mask, marker')) return false;
122
+ if (selectableElement(root, node, outputRect, options)) return false;
123
+ const rect = node.getBoundingClientRect();
124
+ if (rect.width > 0 && Number(getComputedStyle(node).opacity) !== 0
125
+ && drawsSelectionContent(node)) return true;
126
+ return [...node.children].some(visit);
127
+ };
128
+ return [...element.children].some(visit);
129
+ }
130
+
131
+ function selectableContainer(root, element, outputRect, bounds, options = {}) {
132
+ if (!isSelectionContainer(element)) return true;
133
+ if (!bounds) return false;
134
+ // Keep an animated wrapper reachable while one of its painted phases is visible.
135
+ if (hasUncoveredPaintedContent(root, element, outputRect, options)) return true;
136
+ if (selectableContentBranches(root, element, outputRect) <= 1) return false;
137
+ const box = element.getBoundingClientRect();
138
+ return !(outputRect && (box.width >= outputRect.width * 0.5 || box.height >= outputRect.height * 0.5)
139
+ && (box.width >= bounds.width * 1.5 || box.height >= bounds.height * 1.5));
140
+ }
141
+
142
+ function selectableElement(root, element, outputRect, options = {}) {
143
+ const memo = options.selectableMemo ??= new Map();
144
+ if (memo.has(element)) return memo.get(element);
21
145
  if (!root || !element || element === root || !root.contains(element) || isRuntimeElement(element)
22
146
  || element.closest('svg') !== (element.tagName.toLowerCase() === 'svg' ? element : null)
23
- || !elementAddress(root, element)) return false;
147
+ || !elementAddress(root, element)) { memo.set(element, false); return false; }
24
148
  const rect = element.getBoundingClientRect();
25
- if (!(rect.width > 1 && rect.height > 1)) return false;
149
+ if (!(rect.width > 1 && rect.height > 1)) { memo.set(element, false); return false; }
26
150
  for (let ancestor = element; ancestor && root.contains(ancestor); ancestor = ancestor.parentElement) {
27
151
  const style = getComputedStyle(ancestor);
28
- if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity) === 0) return false;
152
+ if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity) === 0) {
153
+ memo.set(element, false); return false;
154
+ }
29
155
  }
156
+ const bounds = selectionBoundsFor(element, options);
157
+ const coverage = bounds ?? rect;
30
158
  const replaced = ['img', 'video', 'canvas', 'svg'].includes(element.tagName.toLowerCase());
31
- return replaced || !outputRect || rect.width < outputRect.width * 0.95
32
- || rect.height < outputRect.height * 0.95;
159
+ const eligible = (replaced || !outputRect || coverage.width < outputRect.width * 0.95
160
+ || coverage.height < outputRect.height * 0.95)
161
+ && selectableContainer(root, element, outputRect, bounds, options);
162
+ memo.set(element, eligible);
163
+ return eligible;
33
164
  }
34
165
 
35
- function nearestSelectableElement(root, hit, outputRect) {
166
+ function nearestSelectableElement(root, hit, outputRect, options = {}) {
167
+ options = { ...options, selectableMemo: new Map(), boundsMemo: new Map() };
36
168
  for (let element = hit; element && element !== root; element = element.parentElement) {
37
- if (selectableElement(root, element, outputRect)) return element;
169
+ if (selectableElement(root, element, outputRect, options)) return element;
38
170
  }
39
171
  return null;
40
172
  }
41
173
 
42
- function firstSelectableElement(root, outputRect) {
43
- return [root, ...root.querySelectorAll('*')].find(element => selectableElement(root, element, outputRect)) ?? null;
174
+ function firstSelectableElement(root, outputRect, options = {}) {
175
+ options = { ...options, selectableMemo: new Map(), boundsMemo: new Map() };
176
+ return [root, ...root.querySelectorAll('*')].find(element => selectableElement(root, element, outputRect, options)) ?? null;
44
177
  }
45
178
 
46
179
  function elementByAddress(root, ref) {
@@ -57,5 +190,6 @@ function elementLabel(root, element) {
57
190
  return siblings.length > 1 ? `${name} ${siblings.indexOf(element) + 1}` : name;
58
191
  }
59
192
  // END element-selection
60
- export { isRuntimeElement, elementAddress, selectableElement, nearestSelectableElement,
193
+ export { isRuntimeElement, elementAddress, drawsSelectionContent, selectionContentBounds,
194
+ isSelectionContainer, selectableContainer, selectableElement, nearestSelectableElement,
61
195
  firstSelectableElement, elementByAddress, elementLabel };
@@ -122,30 +122,163 @@ function elementAddress(root, element) {
122
122
  return index < 0 ? null : `${id ? '#' : '.'}${id || token}[${index}]`;
123
123
  }
124
124
 
125
- function selectableElement(root, element, outputRect) {
125
+ function drawsSelectionContent(element, style = getComputedStyle(element)) {
126
+ const tag = element.tagName.toUpperCase();
127
+ if (['BASE', 'HEAD', 'LINK', 'META', 'NOSCRIPT', 'SCRIPT', 'STYLE', 'TEMPLATE', 'TITLE'].includes(tag)) return false;
128
+ if (['AUDIO', 'CANVAS', 'EMBED', 'IFRAME', 'IMG', 'OBJECT', 'SVG', 'VIDEO'].includes(tag)) return true;
129
+ if (element.namespaceURI === 'http://www.w3.org/2000/svg'
130
+ && ['path', 'rect', 'circle', 'ellipse', 'line', 'polyline', 'polygon', 'use', 'text', 'image'].includes(tag.toLowerCase())) return true;
131
+ if ([...element.childNodes].some(node => node.nodeType === 3 && node.textContent.trim())) return true;
132
+ const transparent = value => {
133
+ const color = String(value ?? '').trim().toLowerCase();
134
+ if (!color || color === 'transparent') return true;
135
+ const legacy = color.match(/^(?:rgba|hsla)\([^)]*,\s*([\d.]+)\)$/);
136
+ if (legacy) return Number(legacy[1]) <= 0;
137
+ const modern = color.match(/\/\s*([\d.]+)%?\s*\)$/);
138
+ return Boolean(modern && Number(modern[1]) <= 0);
139
+ };
140
+ const shadow = String(style.boxShadow ?? '').trim().toLowerCase();
141
+ const shadowColors = shadow.match(/(?:rgba?|hsla?|color)\([^)]*\)|transparent/g) ?? [];
142
+ if (!transparent(style.backgroundColor) || style.backgroundImage && style.backgroundImage !== 'none'
143
+ || shadow && shadow !== 'none' && (shadowColors.length === 0 || shadowColors.some(color => !transparent(color)))) return true;
144
+ for (const side of ['Top', 'Right', 'Bottom', 'Left']) {
145
+ if (parseFloat(style[`border${side}Width`]) > 0
146
+ && !['none', 'hidden'].includes(style[`border${side}Style`])
147
+ && !transparent(style[`border${side}Color`])) return true;
148
+ }
149
+ return parseFloat(style.outlineWidth) > 0 && !['none', 'hidden'].includes(style.outlineStyle)
150
+ && !transparent(style.outlineColor);
151
+ }
152
+
153
+ function selectionVisibleRect(rect, element, container) {
154
+ if (!rect || !(rect.width > 0 && rect.height > 0)) return null;
155
+ let { left, top, right, bottom } = rect;
156
+ for (let node = element; node && node !== container; node = node.parentElement) {
157
+ const style = getComputedStyle(node);
158
+ const paint = /\b(?:paint|content|strict)\b/.test(style.contain ?? '');
159
+ const clipX = paint || node.tagName.toLowerCase() === 'svg' || (style.overflowX || style.overflow) !== 'visible';
160
+ const clipY = paint || node.tagName.toLowerCase() === 'svg' || (style.overflowY || style.overflow) !== 'visible';
161
+ if (clipX || clipY) {
162
+ const clip = node.getBoundingClientRect();
163
+ if (clipX) { left = Math.max(left, clip.left); right = Math.min(right, clip.right); }
164
+ if (clipY) { top = Math.max(top, clip.top); bottom = Math.min(bottom, clip.bottom); }
165
+ }
166
+ if (right <= left || bottom <= top) return null;
167
+ }
168
+ return { left, top, right, bottom, width: right - left, height: bottom - top };
169
+ }
170
+
171
+ function selectionContentBounds(element, options = {}) {
172
+ if (!element) return null;
173
+ let left = Infinity, top = Infinity, right = -Infinity, bottom = -Infinity;
174
+ for (const node of [element, ...element.querySelectorAll('*')]) {
175
+ if (node.closest('[data-akari-interaction]') || node.closest('defs, symbol, pattern, clipPath, mask, marker')) continue;
176
+ let visible = true;
177
+ for (let ancestor = node; ancestor && ancestor !== options.container; ancestor = ancestor.parentElement) {
178
+ const style = getComputedStyle(ancestor);
179
+ if (style.display === 'none' || ['hidden', 'collapse'].includes(style.visibility)
180
+ || Number(style.opacity) === 0) { visible = false; break; }
181
+ }
182
+ if (!visible || !drawsSelectionContent(node)) continue;
183
+ if (node.tagName.toLowerCase() === 'svg'
184
+ && node.querySelector('path, rect, circle, ellipse, line, polyline, polygon, use, text, image')
185
+ && getComputedStyle(node).backgroundColor === 'rgba(0, 0, 0, 0)') continue;
186
+ const raw = options.rawRect?.(node) ?? node.getBoundingClientRect();
187
+ const rect = options.visibleRect ? options.visibleRect(raw, node)
188
+ : selectionVisibleRect(raw, node, options.container);
189
+ if (!rect || !(rect.width > 0 && rect.height > 0)) continue;
190
+ left = Math.min(left, rect.left); top = Math.min(top, rect.top);
191
+ right = Math.max(right, rect.right); bottom = Math.max(bottom, rect.bottom);
192
+ }
193
+ return Number.isFinite(left) ? { left, top, right, bottom, width: right - left, height: bottom - top } : null;
194
+ }
195
+
196
+ function selectableContentBranches(root, element, outputRect) {
197
+ let count = 0;
198
+ const visit = node => {
199
+ if (count > 1) return;
200
+ const style = getComputedStyle(node);
201
+ if (style.display === 'none' || ['hidden', 'collapse'].includes(style.visibility)
202
+ || Number(style.opacity) === 0 || isRuntimeElement(node)) return;
203
+ const rect = node.getBoundingClientRect();
204
+ if (node !== element && elementAddress(root, node) && rect.width > 1 && rect.height > 1
205
+ && drawsSelectionContent(node) && (!outputRect || rect.width < outputRect.width * 0.95
206
+ || rect.height < outputRect.height * 0.95)) { count++; return; }
207
+ for (const child of node.children) visit(child);
208
+ };
209
+ for (const child of element.children) visit(child);
210
+ return count;
211
+ }
212
+
213
+ function isSelectionContainer(element) {
214
+ return Boolean(element?.children?.length) && !drawsSelectionContent(element);
215
+ }
216
+
217
+ function selectionBoundsFor(element, options) {
218
+ const memo = options.boundsMemo ??= new Map();
219
+ if (!memo.has(element)) memo.set(element,
220
+ options.boundsFor ? options.boundsFor(element) : selectionContentBounds(element));
221
+ return memo.get(element);
222
+ }
223
+
224
+ function hasUncoveredPaintedContent(root, element, outputRect, options) {
225
+ const visit = node => {
226
+ if (node.closest('[data-akari-interaction], defs, symbol, pattern, clipPath, mask, marker')) return false;
227
+ if (selectableElement(root, node, outputRect, options)) return false;
228
+ const rect = node.getBoundingClientRect();
229
+ if (rect.width > 0 && Number(getComputedStyle(node).opacity) !== 0
230
+ && drawsSelectionContent(node)) return true;
231
+ return [...node.children].some(visit);
232
+ };
233
+ return [...element.children].some(visit);
234
+ }
235
+
236
+ function selectableContainer(root, element, outputRect, bounds, options = {}) {
237
+ if (!isSelectionContainer(element)) return true;
238
+ if (!bounds) return false;
239
+ // Keep an animated wrapper reachable while one of its painted phases is visible.
240
+ if (hasUncoveredPaintedContent(root, element, outputRect, options)) return true;
241
+ if (selectableContentBranches(root, element, outputRect) <= 1) return false;
242
+ const box = element.getBoundingClientRect();
243
+ return !(outputRect && (box.width >= outputRect.width * 0.5 || box.height >= outputRect.height * 0.5)
244
+ && (box.width >= bounds.width * 1.5 || box.height >= bounds.height * 1.5));
245
+ }
246
+
247
+ function selectableElement(root, element, outputRect, options = {}) {
248
+ const memo = options.selectableMemo ??= new Map();
249
+ if (memo.has(element)) return memo.get(element);
126
250
  if (!root || !element || element === root || !root.contains(element) || isRuntimeElement(element)
127
251
  || element.closest('svg') !== (element.tagName.toLowerCase() === 'svg' ? element : null)
128
- || !elementAddress(root, element)) return false;
252
+ || !elementAddress(root, element)) { memo.set(element, false); return false; }
129
253
  const rect = element.getBoundingClientRect();
130
- if (!(rect.width > 1 && rect.height > 1)) return false;
254
+ if (!(rect.width > 1 && rect.height > 1)) { memo.set(element, false); return false; }
131
255
  for (let ancestor = element; ancestor && root.contains(ancestor); ancestor = ancestor.parentElement) {
132
256
  const style = getComputedStyle(ancestor);
133
- if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity) === 0) return false;
257
+ if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity) === 0) {
258
+ memo.set(element, false); return false;
259
+ }
134
260
  }
261
+ const bounds = selectionBoundsFor(element, options);
262
+ const coverage = bounds ?? rect;
135
263
  const replaced = ['img', 'video', 'canvas', 'svg'].includes(element.tagName.toLowerCase());
136
- return replaced || !outputRect || rect.width < outputRect.width * 0.95
137
- || rect.height < outputRect.height * 0.95;
264
+ const eligible = (replaced || !outputRect || coverage.width < outputRect.width * 0.95
265
+ || coverage.height < outputRect.height * 0.95)
266
+ && selectableContainer(root, element, outputRect, bounds, options);
267
+ memo.set(element, eligible);
268
+ return eligible;
138
269
  }
139
270
 
140
- function nearestSelectableElement(root, hit, outputRect) {
271
+ function nearestSelectableElement(root, hit, outputRect, options = {}) {
272
+ options = { ...options, selectableMemo: new Map(), boundsMemo: new Map() };
141
273
  for (let element = hit; element && element !== root; element = element.parentElement) {
142
- if (selectableElement(root, element, outputRect)) return element;
274
+ if (selectableElement(root, element, outputRect, options)) return element;
143
275
  }
144
276
  return null;
145
277
  }
146
278
 
147
- function firstSelectableElement(root, outputRect) {
148
- return [root, ...root.querySelectorAll('*')].find(element => selectableElement(root, element, outputRect)) ?? null;
279
+ function firstSelectableElement(root, outputRect, options = {}) {
280
+ options = { ...options, selectableMemo: new Map(), boundsMemo: new Map() };
281
+ return [root, ...root.querySelectorAll('*')].find(element => selectableElement(root, element, outputRect, options)) ?? null;
149
282
  }
150
283
 
151
284
  function elementByAddress(root, ref) {
@@ -576,6 +709,21 @@ function elementBoxCompanions(style, parentStyle, width, height) {
576
709
  return null;
577
710
  }
578
711
 
712
+ function contentBounds(element, container) {
713
+ return selectionContentBounds(element, {
714
+ container,
715
+ visibleRect: (rect, node) => visibleFragmentRect(rect, node, container),
716
+ rawRect: node => node.tagName.toUpperCase() === "CANVAS"
717
+ ? (canvasHasDecoration(node) ? node.getBoundingClientRect()
718
+ : window.akari.threeRuntime?.contentBounds?.(node)
719
+ ?? measureCanvasBounds(node) ?? node.getBoundingClientRect())
720
+ : node.getBoundingClientRect(),
721
+ });
722
+ }
723
+ function selectionOptions(root) {
724
+ return { boundsFor: element => contentBounds(element, root?.parentElement) };
725
+ }
726
+
579
727
  // 断片ルートは「inset:0 全画面ラッパー + flex/絶対配置」パターン(overlay-authoring
580
728
  // 規約・text-behind-person.md 等)を許容するため、ルート自身の矩形がコンテナ(断片の
581
729
  // 親 = [data-overlay-id] 要素。overlay-runtime.js の mount() が inset:0 で全画面付与)
@@ -594,7 +742,7 @@ function elementBoxCompanions(style, parentStyle, width, height) {
594
742
  );
595
743
  }
596
744
 
597
- function fragmentBounds(container) {
745
+ function legacyFragmentBounds(container) {
598
746
  const root = fragmentRoot(container);
599
747
  if (!root) return null;
600
748
 
@@ -684,6 +832,13 @@ function elementBoxCompanions(style, parentStyle, width, height) {
684
832
  return { left, top, right, bottom, width: right - left, height: bottom - top };
685
833
  }
686
834
 
835
+ function fragmentBounds(container) {
836
+ const root = fragmentRoot(container);
837
+ if (!root) return null;
838
+ if (drawsOwnContent(root, getComputedStyle(root))) return legacyFragmentBounds(container);
839
+ return contentBounds(root, container) ?? legacyFragmentBounds(container);
840
+ }
841
+
687
842
  function svgReferenceRect(element, value) {
688
843
  const id = String(value ?? '').match(/url\(["']?(?:[^#)]*#)([^)"']+)["']?\)/)?.[1];
689
844
  const svg = element.namespaceURI === 'http://www.w3.org/2000/svg'
@@ -1507,7 +1662,7 @@ function elementBoxCompanions(style, parentStyle, width, height) {
1507
1662
  const transform = readTransform(selectedOverlay);
1508
1663
  const lineRect = lineFrameGeometry(selectedOverlay);
1509
1664
  const focused = focusedElement();
1510
- const focusedGeometry = focused ? elementScreenGeometry(focused) : null;
1665
+ const focusedGeometry = focused ? focusedSelectionGeometry(focused) : null;
1511
1666
  const focusedWidth = focusedGeometry && focusedGeometry.width * Math.hypot(focusedGeometry.axes.x.x, focusedGeometry.axes.x.y);
1512
1667
  const focusedHeight = focusedGeometry && focusedGeometry.height * Math.hypot(focusedGeometry.axes.y.x, focusedGeometry.axes.y.y);
1513
1668
  const focusedAngle = focusedGeometry && Math.atan2(focusedGeometry.axes.x.y, focusedGeometry.axes.x.x) * 180 / Math.PI;
@@ -1564,7 +1719,7 @@ function elementBoxCompanions(style, parentStyle, width, height) {
1564
1719
  }
1565
1720
  const layout = elementHandleLayout(focusedWidth, focusedHeight);
1566
1721
  const name = [...handle.classList].find(token => /^is-(?:n|e|s|w|nw|ne|se|sw|rotate|move)$/.test(token))?.slice(3);
1567
- handle.style.display = name === 'move' ||
1722
+ handle.style.display = name === 'move' || (isSelectionContainer(focused) && name !== 'rotate') ||
1568
1723
  (['n', 's'].includes(name) && layout.hideHorizontalEdges) ||
1569
1724
  (['e', 'w'].includes(name) && layout.hideVerticalEdges) ? 'none' : '';
1570
1725
  if (name === 'rotate') {
@@ -1575,6 +1730,11 @@ function elementBoxCompanions(style, parentStyle, width, height) {
1575
1730
  const y = name.includes('n') ? -layout.cornerOffsetY : layout.cornerOffsetY;
1576
1731
  if (x || y) handle.style.translate = `${x}px ${y}px`;
1577
1732
  else handle.style.removeProperty('translate');
1733
+ } else if (['n', 's', 'e', 'w'].includes(name)) {
1734
+ const x = name === 'w' ? -layout.cornerOffsetX : name === 'e' ? layout.cornerOffsetX : 0;
1735
+ const y = name === 'n' ? -layout.cornerOffsetY : name === 's' ? layout.cornerOffsetY : 0;
1736
+ if (x || y) handle.style.translate = `${x}px ${y}px`;
1737
+ else handle.style.removeProperty('translate');
1578
1738
  }
1579
1739
  if (name && name !== 'move' && name !== 'rotate') {
1580
1740
  const directions = ['ew-resize', 'nwse-resize', 'ns-resize', 'nesw-resize'];
@@ -1739,7 +1899,8 @@ function elementBoxCompanions(style, parentStyle, width, height) {
1739
1899
  }
1740
1900
  function focusedElement() {
1741
1901
  if (!selectedElementFocus() || !selectedOverlay) return null;
1742
- return elementByAddress(fragmentRoot(selectedOverlay), elementFocus.ref);
1902
+ const root = fragmentRoot(selectedOverlay);
1903
+ return elementByAddress(root, elementFocus.ref);
1743
1904
  }
1744
1905
  function selectedElementFocus() {
1745
1906
  return elementFocus?.overlayId === selectedId
@@ -1748,10 +1909,23 @@ function elementBoxCompanions(style, parentStyle, width, height) {
1748
1909
  function reconcileElementFocus() {
1749
1910
  if (!selectedElementFocus()) return;
1750
1911
  const root = fragmentRoot(selectedOverlay);
1751
- if (root && !elementByAddress(root, elementFocus.ref)) focusElement(null);
1912
+ if (!root) return;
1913
+ const ref = elementFocus.ref;
1914
+ const element = elementByAddress(root, ref);
1915
+ if (!element) { focusElement(null); return; }
1916
+ if (selectedOverlay?.isConnected && !selectableElement(root, element, stage?.getBoundingClientRect(), selectionOptions(root))) {
1917
+ requestAnimationFrame(() => {
1918
+ if (elementFocus?.ref === ref && selectedOverlay?.isConnected) {
1919
+ const currentRoot = fragmentRoot(selectedOverlay);
1920
+ const current = elementByAddress(currentRoot, ref);
1921
+ if (!current || !selectableElement(currentRoot, current, stage?.getBoundingClientRect(), selectionOptions(currentRoot))) focusElement(null);
1922
+ }
1923
+ });
1924
+ }
1752
1925
  }
1753
1926
  function focusElement(element, { notify = true } = {}) {
1754
1927
  const root = fragmentRoot(selectedOverlay);
1928
+ if (element && !selectableElement(root, element, stage?.getBoundingClientRect(), selectionOptions(root))) element = null;
1755
1929
  const ref = element && elementAddress(root, element);
1756
1930
  const next = ref ? { overlayId: selectedId, ref, tag: element.tagName.toLowerCase(),
1757
1931
  label: elementLabel(root, element) } : null;
@@ -1770,7 +1944,7 @@ function elementBoxCompanions(style, parentStyle, width, height) {
1770
1944
  const root = fragmentRoot(container);
1771
1945
  const hit = event.target instanceof Element && root?.contains(event.target) ? event.target
1772
1946
  : document.elementsFromPoint(event.clientX, event.clientY).find(element => root?.contains(element)) ?? null;
1773
- focusElement(nearestSelectableElement(root, hit, stage?.getBoundingClientRect()));
1947
+ focusElement(nearestSelectableElement(root, hit, stage?.getBoundingClientRect(), selectionOptions(root)));
1774
1948
  }
1775
1949
  function focusElementAtPoint(overlayId, clientX, clientY, attempt = 0) {
1776
1950
  if (window.akari.capabilities?.elementSelection !== true || !Number.isFinite(clientX)
@@ -1789,7 +1963,7 @@ function elementBoxCompanions(style, parentStyle, width, height) {
1789
1963
  return false;
1790
1964
  }
1791
1965
  const hit = document.elementsFromPoint(clientX, clientY).find(element => root.contains(element)) ?? null;
1792
- focusElement(nearestSelectableElement(root, hit, stage?.getBoundingClientRect()));
1966
+ focusElement(nearestSelectableElement(root, hit, stage?.getBoundingClientRect(), selectionOptions(root)));
1793
1967
  return true;
1794
1968
  }
1795
1969
 
@@ -1873,7 +2047,7 @@ function elementBoxCompanions(style, parentStyle, width, height) {
1873
2047
  const element = target.element?.isConnected ? target.element
1874
2048
  : elementByAddress(fragmentRoot(containerById(target.overlayId)), target.ref);
1875
2049
  if (!element?.isConnected) return null;
1876
- const geometry = elementScreenGeometry(element);
2050
+ const geometry = focusedSelectionGeometry(element);
1877
2051
  const width = geometry.width * Math.hypot(geometry.axes.x.x, geometry.axes.x.y);
1878
2052
  const height = geometry.height * Math.hypot(geometry.axes.y.x, geometry.axes.y.y);
1879
2053
  return { rect: { left: geometry.center.x - width / 2, top: geometry.center.y - height / 2,
@@ -1900,7 +2074,7 @@ function elementBoxCompanions(style, parentStyle, width, height) {
1900
2074
  const root = fragmentRoot(selectedOverlay);
1901
2075
  const path = [];
1902
2076
  for (let node = focusedElement(); node && node !== root; node = node.parentElement) {
1903
- if (selectableElement(root, node, stage?.getBoundingClientRect())) path.unshift(node);
2077
+ if (selectableElement(root, node, stage?.getBoundingClientRect(), selectionOptions(root))) path.unshift(node);
1904
2078
  }
1905
2079
  for (const node of path) append(elementLabel(root, node), () => focusElement(node),
1906
2080
  { kind: 'element', element: node, overlayId: selectedId, ref: elementAddress(root, node) });
@@ -2391,6 +2565,48 @@ function elementBoxCompanions(style, parentStyle, width, height) {
2391
2565
  elementGeometryCache = { element, signature, geometry };
2392
2566
  return geometry;
2393
2567
  }
2568
+ function focusedSelectionGeometry(element) {
2569
+ const geometry = elementScreenGeometry(element);
2570
+ if (!isSelectionContainer(element)) return geometry;
2571
+ const container = selectedOverlay ?? element.parentElement;
2572
+ const bounds = contentBounds(element, container);
2573
+ if (!bounds) return geometry;
2574
+ const scaleX = Math.hypot(geometry.axes.x.x, geometry.axes.x.y);
2575
+ const scaleY = Math.hypot(geometry.axes.y.x, geometry.axes.y.y);
2576
+ if (!(scaleX > 0 && scaleY > 0)) return geometry;
2577
+ const ux = { x: geometry.axes.x.x / scaleX, y: geometry.axes.x.y / scaleX };
2578
+ const uy = { x: geometry.axes.y.x / scaleY, y: geometry.axes.y.y / scaleY };
2579
+ const angle = Math.abs(Math.atan2(ux.y, ux.x));
2580
+ if (angle < 0.001) return { ...geometry,
2581
+ center: { x: bounds.left + bounds.width / 2, y: bounds.top + bounds.height / 2 },
2582
+ width: bounds.width / scaleX, height: bounds.height / scaleY };
2583
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
2584
+ for (const node of element.querySelectorAll('*')) {
2585
+ if (!drawsSelectionContent(node) || !isPaintedElement(node, container)) continue;
2586
+ const rect = visibleFragmentRect(node.getBoundingClientRect(), node, container);
2587
+ if (!rect) continue;
2588
+ const center = { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
2589
+ const dx = center.x - geometry.center.x, dy = center.y - geometry.center.y;
2590
+ const x = dx * ux.x + dy * ux.y, y = dx * uy.x + dy * uy.y;
2591
+ const halfX = (node.offsetWidth || rect.width / scaleX) * scaleX / 2;
2592
+ const halfY = (node.offsetHeight || rect.height / scaleY) * scaleY / 2;
2593
+ minX = Math.min(minX, x - halfX); maxX = Math.max(maxX, x + halfX);
2594
+ minY = Math.min(minY, y - halfY); maxY = Math.max(maxY, y + halfY);
2595
+ }
2596
+ if (!Number.isFinite(minX)) return geometry;
2597
+ const midX = (minX + maxX) / 2, midY = (minY + maxY) / 2;
2598
+ return { ...geometry, center: { x: geometry.center.x + midX * ux.x + midY * uy.x,
2599
+ y: geometry.center.y + midX * ux.y + midY * uy.y },
2600
+ width: (maxX - minX) / scaleX, height: (maxY - minY) / scaleY };
2601
+ }
2602
+ function focusedSelectionRect(element) {
2603
+ const geometry = focusedSelectionGeometry(element);
2604
+ const width = geometry.width * Math.hypot(geometry.axes.x.x, geometry.axes.x.y);
2605
+ const height = geometry.height * Math.hypot(geometry.axes.y.x, geometry.axes.y.y);
2606
+ return { left: geometry.center.x - width / 2, top: geometry.center.y - height / 2,
2607
+ right: geometry.center.x + width / 2, bottom: geometry.center.y + height / 2,
2608
+ width, height };
2609
+ }
2394
2610
  function moveElementPointTo(gesture, horizontal, vertical, target) {
2395
2611
  const geometry = elementScreenGeometry(gesture.element);
2396
2612
  const point = elementPoint(geometry, horizontal, vertical);
@@ -2756,8 +2972,8 @@ function elementBoxCompanions(style, parentStyle, width, height) {
2756
2972
  && (!scoped || scoped.selectId === container.dataset.overlayId)) {
2757
2973
  const root = fragmentRoot(container);
2758
2974
  const hit = event.target instanceof Element && root?.contains(event.target) ? event.target : null;
2759
- const element = nearestSelectableElement(root, hit, stage?.getBoundingClientRect());
2760
- const rect = element?.getBoundingClientRect() ?? fragmentBounds(container);
2975
+ const element = nearestSelectableElement(root, hit, stage?.getBoundingClientRect(), selectionOptions(root));
2976
+ const rect = element ? focusedSelectionRect(element) : fragmentBounds(container);
2761
2977
  if (!rect || selectedOverlay === container
2762
2978
  && selectedElementFocus()?.ref === elementAddress(root, element)) { hideHover(); return; }
2763
2979
  showHoverFrame(rect, null, container.dataset.overlayId, false, false);
@@ -4170,7 +4386,7 @@ function elementBoxCompanions(style, parentStyle, width, height) {
4170
4386
  if (window.akari.capabilities?.elementSelection === true && event.isTrusted && !event.shiftKey) {
4171
4387
  const root = fragmentRoot(hit);
4172
4388
  const target = event.target instanceof Element && root?.contains(event.target)
4173
- ? nearestSelectableElement(root, event.target, stage?.getBoundingClientRect()) : null;
4389
+ ? nearestSelectableElement(root, event.target, stage?.getBoundingClientRect(), selectionOptions(root)) : null;
4174
4390
  clickOrigin.hitId = hit.dataset.overlayId;
4175
4391
  clickOrigin.elementRef = target ? elementAddress(root, target) : null;
4176
4392
  }
@@ -4185,7 +4401,10 @@ function elementBoxCompanions(style, parentStyle, width, height) {
4185
4401
  if (!selectedOverlay || (window.akari.capabilities?.elementSelection === true
4186
4402
  ? selectedId !== hit.dataset.overlayId : selectedOverlay !== hit)) return;
4187
4403
  if (!event.shiftKey) {
4188
- if (clickOrigin?.hitId === selectedId) {
4404
+ const focused = focusedElement();
4405
+ if (focused && isSelectionContainer(focused) && focused.contains(event.target)) {
4406
+ clickOrigin.preservedContainer = true;
4407
+ } else if (clickOrigin?.hitId === selectedId) {
4189
4408
  focusElement(elementByAddress(fragmentRoot(selectedOverlay), clickOrigin.elementRef));
4190
4409
  } else focusElementAt(selectedOverlay, event);
4191
4410
  } else if (selectedElementFocus()) focusElement(null);
@@ -4221,7 +4440,12 @@ function elementBoxCompanions(style, parentStyle, width, height) {
4221
4440
  }
4222
4441
 
4223
4442
  selectOverlay(container);
4224
- if (!event.shiftKey) focusElementAt(container, event);
4443
+ if (!event.shiftKey) {
4444
+ const focused = focusedElement();
4445
+ if (focused && isSelectionContainer(focused) && focused.contains(event.target)) {
4446
+ clickOrigin.preservedContainer = true;
4447
+ } else focusElementAt(container, event);
4448
+ }
4225
4449
  else if (selectedElementFocus()) focusElement(null);
4226
4450
  if (window.akari.capabilities?.elementSelection === true && clickOrigin) {
4227
4451
  clickOrigin.hitId = selectedId;
@@ -4984,7 +5208,8 @@ function elementBoxCompanions(style, parentStyle, width, height) {
4984
5208
  else selectOverlay(hit);
4985
5209
  if (window.akari.capabilities?.elementSelection === true
4986
5210
  && event.isTrusted && !event.shiftKey && selectedOverlay) {
4987
- if (!nextId && clickOrigin?.hitId === selectedId) {
5211
+ if (clickOrigin?.preservedContainer) focusElementAt(selectedOverlay, event);
5212
+ else if (!nextId && clickOrigin?.hitId === selectedId) {
4988
5213
  focusElement(elementByAddress(fragmentRoot(selectedOverlay), clickOrigin.elementRef));
4989
5214
  } else focusElementAt(selectedOverlay, event);
4990
5215
  }
@@ -5052,7 +5277,7 @@ function elementBoxCompanions(style, parentStyle, width, height) {
5052
5277
  if (event.key === 'Escape' || event.key === 'Enter' && event.shiftKey) {
5053
5278
  const root = fragmentRoot(selectedOverlay);
5054
5279
  const parent = nearestSelectableElement(root, focusedElement()?.parentElement,
5055
- stage?.getBoundingClientRect());
5280
+ stage?.getBoundingClientRect(), selectionOptions(root));
5056
5281
  focusElement(parent); stop(); return;
5057
5282
  }
5058
5283
  if ((event.key === 'Delete' || event.key === 'Backspace'
@@ -5064,7 +5289,8 @@ function elementBoxCompanions(style, parentStyle, width, height) {
5064
5289
  }
5065
5290
  if (event.key === 'Enter' && !event.shiftKey && !activeEdit && !selectedElementFocus()
5066
5291
  && selectedOverlay && canFocusElement(selectedOverlay)) {
5067
- const first = firstSelectableElement(fragmentRoot(selectedOverlay), stage?.getBoundingClientRect());
5292
+ const root = fragmentRoot(selectedOverlay);
5293
+ const first = firstSelectableElement(root, stage?.getBoundingClientRect(), selectionOptions(root));
5068
5294
  if (first) {
5069
5295
  focusElement(first);
5070
5296
  event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation();