@lynx-js/web-elements 0.12.8 → 0.12.10
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 +27 -0
- package/README.md +14 -0
- package/dist/elements/ScrollView/ScrollViewMouseDrag.d.ts +1 -0
- package/dist/elements/ScrollView/ScrollViewMouseDrag.js +315 -0
- package/dist/elements/XFoldViewNg/XFoldviewNg.js +3 -1
- package/dist/elements/XText/InlineText.d.ts +1 -1
- package/dist/elements/XText/InlineText.js +3 -2
- package/dist/elements/XText/XText.js +7 -1
- package/dist/elements/XText/XTextSelectionEvents.d.ts +9 -0
- package/dist/elements/XText/XTextSelectionEvents.js +134 -0
- package/dist/elements/XText/index.d.ts +1 -0
- package/dist/elements/XText/index.js +1 -0
- package/package.json +8 -3
- package/src/elements/XFoldViewNg/x-foldview-ng.css +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# @lynx-js/web-elements
|
|
2
2
|
|
|
3
|
+
## 0.12.10
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Update markdown-it to ^15.0.1. ([#3749](https://github.com/lynx-family/lynx-stack/pull/3749))
|
|
8
|
+
|
|
9
|
+
- Add an optional `scroll-view` mouse-drag plugin for touchscreen-like scrolling ([#3594](https://github.com/lynx-family/lynx-stack/pull/3594))
|
|
10
|
+
on desktop browsers. Enable it by importing
|
|
11
|
+
`@lynx-js/web-core/plugins/scroll-view-mouse-drag` or
|
|
12
|
+
`@lynx-js/web-elements/plugins/scroll-view-mouse-drag` before registering the
|
|
13
|
+
web elements.
|
|
14
|
+
- Support the `bindselectionchange` event on `text` and `inline-text` in Lynx for Web. ([#3741](https://github.com/lynx-family/lynx-stack/pull/3741))
|
|
15
|
+
|
|
16
|
+
## 0.12.9
|
|
17
|
+
|
|
18
|
+
### Patch Changes
|
|
19
|
+
|
|
20
|
+
- Clamp the `setFoldExpanded` offset of `<x-foldview-ng>` to the scrollable length. ([#3290](https://github.com/lynx-family/lynx-stack/pull/3290))
|
|
21
|
+
|
|
22
|
+
`setFoldExpanded` called the native `scrollTo`, bypassing the clamping done by the
|
|
23
|
+
`scrollTop` setter. A page collapsing its header with a deliberately large offset
|
|
24
|
+
(e.g. `offset: '99999px'`) scrolled past the end, which does not happen on native.
|
|
25
|
+
- Give `<x-foldview-header-ng>` a `width: 100%`. ([#3290](https://github.com/lynx-family/lynx-stack/pull/3290))
|
|
26
|
+
|
|
27
|
+
The element is laid out with `position: absolute` but had no width, so it shrank to
|
|
28
|
+
fit its content instead of filling the foldview.
|
|
29
|
+
|
|
3
30
|
## 0.12.8
|
|
4
31
|
|
|
5
32
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -37,6 +37,20 @@ document.body.innerHTML = `
|
|
|
37
37
|
`;
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
+
### Mouse-drag scrolling
|
|
41
|
+
|
|
42
|
+
To make `<scroll-view>` respond to mouse dragging like a touchscreen, load
|
|
43
|
+
the optional plugin before registering the elements:
|
|
44
|
+
|
|
45
|
+
```javascript
|
|
46
|
+
import '@lynx-js/web-elements/plugins/scroll-view-mouse-drag';
|
|
47
|
+
import '@lynx-js/web-elements/all';
|
|
48
|
+
import '@lynx-js/web-elements/index.css';
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The plugin only changes `<scroll-view>` and leaves native touch scrolling
|
|
52
|
+
unchanged.
|
|
53
|
+
|
|
40
54
|
## Document
|
|
41
55
|
|
|
42
56
|
See our website for more information.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
// Copyright 2026 The Lynx Authors. All rights reserved.
|
|
2
|
+
// Licensed under the Apache License Version 2.0 that can be found in the
|
|
3
|
+
// LICENSE file in the root directory of this source tree.
|
|
4
|
+
import { boostedQueueMicrotask } from '../../element-reactive/index.js';
|
|
5
|
+
const dragStartThreshold = 5;
|
|
6
|
+
const userScrollableOverflowValues = new Set(['auto', 'overlay', 'scroll']);
|
|
7
|
+
const blockingOverscrollBehaviorValues = new Set(['contain', 'none']);
|
|
8
|
+
function getScrollableAxes(dom, style) {
|
|
9
|
+
if (dom.getAttribute('enable-scroll') === 'false') {
|
|
10
|
+
return { x: false, y: false };
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
x: userScrollableOverflowValues.has(style.overflowX)
|
|
14
|
+
&& dom.scrollWidth - dom.clientWidth > 1,
|
|
15
|
+
y: userScrollableOverflowValues.has(style.overflowY)
|
|
16
|
+
&& dom.scrollHeight - dom.clientHeight > 1,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function findScrollChain(event) {
|
|
20
|
+
let owner;
|
|
21
|
+
let ownerAxes;
|
|
22
|
+
const x = [];
|
|
23
|
+
const y = [];
|
|
24
|
+
for (const target of event.composedPath()) {
|
|
25
|
+
if (!(target instanceof HTMLElement) || target.localName !== 'scroll-view') {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const style = getComputedStyle(target);
|
|
29
|
+
const axes = getScrollableAxes(target, style);
|
|
30
|
+
if (!axes.x && !axes.y) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (!owner) {
|
|
34
|
+
owner = target;
|
|
35
|
+
ownerAxes = axes;
|
|
36
|
+
}
|
|
37
|
+
if (axes.x) {
|
|
38
|
+
x.push({
|
|
39
|
+
allowsChaining: !blockingOverscrollBehaviorValues.has(style.overscrollBehaviorX),
|
|
40
|
+
dom: target,
|
|
41
|
+
start: target.scrollLeft,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
if (axes.y) {
|
|
45
|
+
y.push({
|
|
46
|
+
allowsChaining: !blockingOverscrollBehaviorValues.has(style.overscrollBehaviorY),
|
|
47
|
+
dom: target,
|
|
48
|
+
start: target.scrollTop,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return owner && ownerAxes ? { owner, ownerAxes, x, y } : undefined;
|
|
53
|
+
}
|
|
54
|
+
function selectDragAxes(chain, deltaX, deltaY) {
|
|
55
|
+
const absoluteX = Math.abs(deltaX);
|
|
56
|
+
const absoluteY = Math.abs(deltaY);
|
|
57
|
+
if (Math.hypot(deltaX, deltaY) <= dragStartThreshold) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
const hasX = chain.x.length > 0;
|
|
61
|
+
const hasY = chain.y.length > 0;
|
|
62
|
+
if (chain.ownerAxes.x && chain.ownerAxes.y) {
|
|
63
|
+
return { x: hasX, y: hasY };
|
|
64
|
+
}
|
|
65
|
+
if (hasX && hasY) {
|
|
66
|
+
if (absoluteX === absoluteY) {
|
|
67
|
+
return chain.ownerAxes.x
|
|
68
|
+
? { x: true, y: false }
|
|
69
|
+
: { x: false, y: true };
|
|
70
|
+
}
|
|
71
|
+
return absoluteX > absoluteY
|
|
72
|
+
? { x: true, y: false }
|
|
73
|
+
: { x: false, y: true };
|
|
74
|
+
}
|
|
75
|
+
if (hasX && absoluteX >= absoluteY) {
|
|
76
|
+
return { x: true, y: false };
|
|
77
|
+
}
|
|
78
|
+
if (hasY && absoluteY >= absoluteX) {
|
|
79
|
+
return { x: false, y: true };
|
|
80
|
+
}
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
function applyScrollDelta(candidates, delta, axis) {
|
|
84
|
+
let remaining = delta;
|
|
85
|
+
for (const candidate of candidates) {
|
|
86
|
+
const next = candidate.start + remaining;
|
|
87
|
+
if (axis === 'x') {
|
|
88
|
+
candidate.dom.scrollTo({
|
|
89
|
+
behavior: 'instant',
|
|
90
|
+
left: next,
|
|
91
|
+
top: candidate.dom.scrollTop,
|
|
92
|
+
});
|
|
93
|
+
remaining -= candidate.dom.scrollLeft - candidate.start;
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
candidate.dom.scrollTo({
|
|
97
|
+
behavior: 'instant',
|
|
98
|
+
left: candidate.dom.scrollLeft,
|
|
99
|
+
top: next,
|
|
100
|
+
});
|
|
101
|
+
remaining -= candidate.dom.scrollTop - candidate.start;
|
|
102
|
+
}
|
|
103
|
+
if (!candidate.allowsChaining) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Makes a scroll-view follow mouse dragging in the same direction as touch
|
|
110
|
+
* scrolling. Pointer events from touchscreens are left to the browser.
|
|
111
|
+
*/
|
|
112
|
+
class ScrollViewMouseDrag {
|
|
113
|
+
static observedAttributes = [];
|
|
114
|
+
#dom;
|
|
115
|
+
#connected = false;
|
|
116
|
+
#dragState;
|
|
117
|
+
#clickSuppression;
|
|
118
|
+
constructor(dom) {
|
|
119
|
+
this.#dom = dom;
|
|
120
|
+
}
|
|
121
|
+
#handlePointerDown = (event) => {
|
|
122
|
+
if (event.pointerType !== 'mouse'
|
|
123
|
+
|| !event.isPrimary
|
|
124
|
+
|| event.button !== 0
|
|
125
|
+
|| this.#dragState) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const chain = findScrollChain(event);
|
|
129
|
+
if (!chain || chain.owner !== this.#dom) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const document = this.#dom.ownerDocument;
|
|
133
|
+
this.#dragState = {
|
|
134
|
+
chain,
|
|
135
|
+
document,
|
|
136
|
+
dragging: false,
|
|
137
|
+
pendingDeltaX: 0,
|
|
138
|
+
pendingDeltaY: 0,
|
|
139
|
+
pointerId: event.pointerId,
|
|
140
|
+
scrollUpdateQueued: false,
|
|
141
|
+
startClientX: event.clientX,
|
|
142
|
+
startClientY: event.clientY,
|
|
143
|
+
};
|
|
144
|
+
this.#trackPotentialClick(document);
|
|
145
|
+
document.addEventListener('pointermove', this.#handlePointerMove, {
|
|
146
|
+
capture: true,
|
|
147
|
+
passive: false,
|
|
148
|
+
});
|
|
149
|
+
document.addEventListener('pointerup', this.#handlePointerUp, true);
|
|
150
|
+
document.addEventListener('pointercancel', this.#handlePointerCancel, true);
|
|
151
|
+
document.defaultView?.addEventListener('blur', this.#handlePointerCancel);
|
|
152
|
+
};
|
|
153
|
+
#handlePointerMove = (event) => {
|
|
154
|
+
const state = this.#dragState;
|
|
155
|
+
if (!state || event.pointerId !== state.pointerId) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if ((event.buttons & 1) === 0) {
|
|
159
|
+
this.#stopDragging(false);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const deltaX = event.clientX - state.startClientX;
|
|
163
|
+
const deltaY = event.clientY - state.startClientY;
|
|
164
|
+
if (!state.dragging) {
|
|
165
|
+
const activeAxes = selectDragAxes(state.chain, deltaX, deltaY);
|
|
166
|
+
if (!activeAxes) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
state.activeAxes = activeAxes;
|
|
170
|
+
state.dragging = true;
|
|
171
|
+
state.previousUserSelect = this.#dom.style.userSelect;
|
|
172
|
+
this.#dom.style.userSelect = 'none';
|
|
173
|
+
state.document.getSelection()?.removeAllRanges();
|
|
174
|
+
this.#dom.setPointerCapture(event.pointerId);
|
|
175
|
+
}
|
|
176
|
+
if (event.cancelable) {
|
|
177
|
+
event.preventDefault();
|
|
178
|
+
}
|
|
179
|
+
state.pendingDeltaX = -deltaX;
|
|
180
|
+
state.pendingDeltaY = -deltaY;
|
|
181
|
+
if (!state.scrollUpdateQueued) {
|
|
182
|
+
state.scrollUpdateQueued = true;
|
|
183
|
+
boostedQueueMicrotask(() => this.#flushScrollUpdate(state));
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
#flushScrollUpdate(state) {
|
|
187
|
+
if (!state.scrollUpdateQueued) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
state.scrollUpdateQueued = false;
|
|
191
|
+
if (state.activeAxes?.x) {
|
|
192
|
+
applyScrollDelta(state.chain.x, state.pendingDeltaX, 'x');
|
|
193
|
+
}
|
|
194
|
+
if (state.activeAxes?.y) {
|
|
195
|
+
applyScrollDelta(state.chain.y, state.pendingDeltaY, 'y');
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
#handlePointerUp = (event) => {
|
|
199
|
+
if (event.pointerId !== this.#dragState?.pointerId) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
this.#stopDragging(this.#dragState.dragging);
|
|
203
|
+
};
|
|
204
|
+
#handlePointerCancel = (event) => {
|
|
205
|
+
if (event instanceof PointerEvent
|
|
206
|
+
&& event.pointerId !== this.#dragState?.pointerId) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
this.#stopDragging(false);
|
|
210
|
+
};
|
|
211
|
+
#handleDragStart = (event) => {
|
|
212
|
+
if (this.#dragState) {
|
|
213
|
+
event.preventDefault();
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
#handleClick = (event) => {
|
|
217
|
+
if (!this.#clickSuppression?.suppress
|
|
218
|
+
|| !event.composedPath().includes(this.#dom)) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
this.#clearClickSuppression();
|
|
222
|
+
event.preventDefault();
|
|
223
|
+
event.stopImmediatePropagation();
|
|
224
|
+
};
|
|
225
|
+
#stopDragging(suppressClick) {
|
|
226
|
+
const state = this.#dragState;
|
|
227
|
+
if (!state) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const { document } = state;
|
|
231
|
+
this.#flushScrollUpdate(state);
|
|
232
|
+
document.removeEventListener('pointermove', this.#handlePointerMove, true);
|
|
233
|
+
document.removeEventListener('pointerup', this.#handlePointerUp, true);
|
|
234
|
+
document.removeEventListener('pointercancel', this.#handlePointerCancel, true);
|
|
235
|
+
document.defaultView?.removeEventListener('blur', this.#handlePointerCancel);
|
|
236
|
+
if (this.#dom.hasPointerCapture(state.pointerId)) {
|
|
237
|
+
this.#dom.releasePointerCapture(state.pointerId);
|
|
238
|
+
}
|
|
239
|
+
if (state.dragging && this.#dom.style.userSelect === 'none') {
|
|
240
|
+
this.#dom.style.userSelect = state.previousUserSelect ?? '';
|
|
241
|
+
}
|
|
242
|
+
this.#dragState = undefined;
|
|
243
|
+
if (suppressClick) {
|
|
244
|
+
const clickSuppression = this.#clickSuppression;
|
|
245
|
+
if (clickSuppression) {
|
|
246
|
+
clickSuppression.suppress = true;
|
|
247
|
+
clearTimeout(clickSuppression.timer);
|
|
248
|
+
clickSuppression.timer = setTimeout(this.#clearClickSuppression, 0);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
this.#clearClickSuppression();
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
#trackPotentialClick(document) {
|
|
256
|
+
this.#clearClickSuppression();
|
|
257
|
+
const target = document.defaultView ?? document;
|
|
258
|
+
this.#clickSuppression = { suppress: false, target };
|
|
259
|
+
target.addEventListener('click', this.#handleClick, true);
|
|
260
|
+
}
|
|
261
|
+
#clearClickSuppression = () => {
|
|
262
|
+
const clickSuppression = this.#clickSuppression;
|
|
263
|
+
if (!clickSuppression) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
clickSuppression.target.removeEventListener('click', this.#handleClick, true);
|
|
267
|
+
clearTimeout(clickSuppression.timer);
|
|
268
|
+
this.#clickSuppression = undefined;
|
|
269
|
+
};
|
|
270
|
+
connectedCallback() {
|
|
271
|
+
if (this.#connected) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
this.#connected = true;
|
|
275
|
+
this.#dom.addEventListener('pointerdown', this.#handlePointerDown, true);
|
|
276
|
+
this.#dom.addEventListener('dragstart', this.#handleDragStart, true);
|
|
277
|
+
}
|
|
278
|
+
dispose() {
|
|
279
|
+
this.#connected = false;
|
|
280
|
+
this.#dom.removeEventListener('pointerdown', this.#handlePointerDown, true);
|
|
281
|
+
this.#dom.removeEventListener('dragstart', this.#handleDragStart, true);
|
|
282
|
+
this.#stopDragging(false);
|
|
283
|
+
this.#clearClickSuppression();
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const scrollViewTagName = 'scroll-view';
|
|
287
|
+
const registeredClasses = new WeakSet();
|
|
288
|
+
function registerMouseDragPlugin(elementClass) {
|
|
289
|
+
if (registeredClasses.has(elementClass)) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const componentClass = elementClass;
|
|
293
|
+
if (!componentClass.registerPlugin) {
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
componentClass.registerPlugin(ScrollViewMouseDrag);
|
|
297
|
+
registeredClasses.add(elementClass);
|
|
298
|
+
}
|
|
299
|
+
if (typeof customElements !== 'undefined') {
|
|
300
|
+
const registeredScrollView = customElements.get(scrollViewTagName);
|
|
301
|
+
if (registeredScrollView) {
|
|
302
|
+
registerMouseDragPlugin(registeredScrollView);
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
const define = Reflect.get(customElements, 'define');
|
|
306
|
+
customElements.define = function (name, elementClass, options) {
|
|
307
|
+
if (name === scrollViewTagName) {
|
|
308
|
+
registerMouseDragPlugin(elementClass);
|
|
309
|
+
}
|
|
310
|
+
Reflect.apply(define, this, [name, elementClass, options]);
|
|
311
|
+
};
|
|
312
|
+
void customElements.whenDefined(scrollViewTagName).then(registerMouseDragPlugin);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
//# sourceMappingURL=ScrollViewMouseDrag.js.map
|
|
@@ -70,8 +70,10 @@ let XFoldviewNg = (() => {
|
|
|
70
70
|
setFoldExpanded(params) {
|
|
71
71
|
const { offset, smooth = true } = params;
|
|
72
72
|
const offsetValue = parseFloat(offset);
|
|
73
|
+
// `scrollTo` is the native method and does not go through the `scrollTop`
|
|
74
|
+
// setter above, so the offset has to be clamped here as well.
|
|
73
75
|
this.scrollTo({
|
|
74
|
-
top: offsetValue,
|
|
76
|
+
top: Math.min(Math.max(offsetValue, 0), this[scrollableLength]),
|
|
75
77
|
behavior: smooth ? 'smooth' : 'instant',
|
|
76
78
|
});
|
|
77
79
|
}
|
|
@@ -5,11 +5,12 @@ import { __esDecorate, __runInitializers } from "tslib";
|
|
|
5
5
|
// LICENSE file in the root directory of this source tree.
|
|
6
6
|
*/
|
|
7
7
|
import { Component } from '../../element-reactive/index.js';
|
|
8
|
+
import { XTextSelectionEvents } from './XTextSelectionEvents.js';
|
|
8
9
|
/**
|
|
9
|
-
* @deprecated
|
|
10
|
+
* @deprecated Use x-text instead of inline-text.
|
|
10
11
|
*/
|
|
11
12
|
let InlineText = (() => {
|
|
12
|
-
let _classDecorators = [Component('inline-text', [])];
|
|
13
|
+
let _classDecorators = [Component('inline-text', [XTextSelectionEvents])];
|
|
13
14
|
let _classDescriptor;
|
|
14
15
|
let _classExtraInitializers = [];
|
|
15
16
|
let _classThis;
|
|
@@ -10,8 +10,14 @@ import { ScrollIntoView } from '../ScrollView/ScrollIntoView.js';
|
|
|
10
10
|
import { RawTextAttributes } from './RawText.js';
|
|
11
11
|
import { CommonEventsAndMethods, layoutChangeTarget, } from '../common/CommonEventsAndMethods.js';
|
|
12
12
|
import { templateXText } from '../htmlTemplates.js';
|
|
13
|
+
import { XTextSelectionEvents } from './XTextSelectionEvents.js';
|
|
13
14
|
let XText = (() => {
|
|
14
|
-
let _classDecorators = [Component('x-text', [
|
|
15
|
+
let _classDecorators = [Component('x-text', [
|
|
16
|
+
CommonEventsAndMethods,
|
|
17
|
+
XTextSelectionEvents,
|
|
18
|
+
XTextTruncation,
|
|
19
|
+
RawTextAttributes,
|
|
20
|
+
], templateXText)];
|
|
15
21
|
let _classDescriptor;
|
|
16
22
|
let _classExtraInitializers = [];
|
|
17
23
|
let _classThis;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type AttributeReactiveClass } from '../../element-reactive/index.js';
|
|
2
|
+
export declare class XTextSelectionEvents implements InstanceType<AttributeReactiveClass<typeof HTMLElement>> {
|
|
3
|
+
#private;
|
|
4
|
+
static observedAttributes: never[];
|
|
5
|
+
constructor(dom: HTMLElement);
|
|
6
|
+
connectedCallback(): void;
|
|
7
|
+
dispose(): void;
|
|
8
|
+
_handleEnableSelectionChangeEvent(status: boolean): void;
|
|
9
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { __esDecorate, __runInitializers } from "tslib";
|
|
2
|
+
/*
|
|
3
|
+
// Copyright 2026 The Lynx Authors. All rights reserved.
|
|
4
|
+
// Licensed under the Apache License Version 2.0 that can be found in the
|
|
5
|
+
// LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
import { registerEventEnableStatusChangeHandler, } from '../../element-reactive/index.js';
|
|
8
|
+
import { commonComponentEventSetting } from '../common/commonEventInitConfiguration.js';
|
|
9
|
+
const emptySelection = () => ({
|
|
10
|
+
start: -1,
|
|
11
|
+
end: -1,
|
|
12
|
+
direction: 'forward',
|
|
13
|
+
});
|
|
14
|
+
/**
|
|
15
|
+
* Converts a selection boundary to an offset in the target's text. Using the
|
|
16
|
+
* range text length provides one offset space across nested text nodes.
|
|
17
|
+
*/
|
|
18
|
+
const getTextOffset = (root, node, offset) => {
|
|
19
|
+
if (!node || !root.contains(node))
|
|
20
|
+
return null;
|
|
21
|
+
const range = root.ownerDocument.createRange();
|
|
22
|
+
range.selectNodeContents(root);
|
|
23
|
+
try {
|
|
24
|
+
range.setEnd(node, offset);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
return range.toString().length;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Returns offsets relative to the target text. A -1 offset means the selection
|
|
33
|
+
* is collapsed, unavailable, or outside the target.
|
|
34
|
+
*/
|
|
35
|
+
const getSelectionDetail = (dom) => {
|
|
36
|
+
const selection = dom.ownerDocument.getSelection();
|
|
37
|
+
if (!selection || selection.rangeCount === 0) {
|
|
38
|
+
return emptySelection();
|
|
39
|
+
}
|
|
40
|
+
const anchor = getTextOffset(dom, selection.anchorNode, selection.anchorOffset);
|
|
41
|
+
const focus = getTextOffset(dom, selection.focusNode, selection.focusOffset);
|
|
42
|
+
if (anchor !== null && focus !== null && anchor !== focus) {
|
|
43
|
+
return {
|
|
44
|
+
start: Math.min(anchor, focus),
|
|
45
|
+
end: Math.max(anchor, focus),
|
|
46
|
+
direction: anchor < focus ? 'forward' : 'backward',
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
// WebKit retargets selection endpoints outside a shadow tree. Ask for the
|
|
50
|
+
// composed range to recover the actual text nodes in that case.
|
|
51
|
+
const shadowRoots = [];
|
|
52
|
+
let root = dom.getRootNode();
|
|
53
|
+
while (root instanceof ShadowRoot) {
|
|
54
|
+
shadowRoots.push(root);
|
|
55
|
+
root = root.host.getRootNode();
|
|
56
|
+
}
|
|
57
|
+
const [range] = selection.getComposedRanges?.({ shadowRoots }) ?? [];
|
|
58
|
+
const start = range
|
|
59
|
+
? getTextOffset(dom, range.startContainer, range.startOffset)
|
|
60
|
+
: null;
|
|
61
|
+
const end = range
|
|
62
|
+
? getTextOffset(dom, range.endContainer, range.endOffset)
|
|
63
|
+
: null;
|
|
64
|
+
if (start === null || end === null || start === end)
|
|
65
|
+
return emptySelection();
|
|
66
|
+
return {
|
|
67
|
+
start,
|
|
68
|
+
end,
|
|
69
|
+
direction: selection.direction === 'backward' ? 'backward' : 'forward',
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
let XTextSelectionEvents = (() => {
|
|
73
|
+
let _instanceExtraInitializers = [];
|
|
74
|
+
let __handleEnableSelectionChangeEvent_decorators;
|
|
75
|
+
return class XTextSelectionEvents {
|
|
76
|
+
static {
|
|
77
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
|
|
78
|
+
__handleEnableSelectionChangeEvent_decorators = [registerEventEnableStatusChangeHandler('selectionchange')];
|
|
79
|
+
__esDecorate(this, null, __handleEnableSelectionChangeEvent_decorators, { kind: "method", name: "_handleEnableSelectionChangeEvent", static: false, private: false, access: { has: obj => "_handleEnableSelectionChangeEvent" in obj, get: obj => obj._handleEnableSelectionChangeEvent }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
80
|
+
if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
81
|
+
}
|
|
82
|
+
static observedAttributes = [];
|
|
83
|
+
#dom = __runInitializers(this, _instanceExtraInitializers);
|
|
84
|
+
#enabled = false;
|
|
85
|
+
#connected = false;
|
|
86
|
+
#attachedDocument;
|
|
87
|
+
#lastSelectionSignature;
|
|
88
|
+
constructor(dom) {
|
|
89
|
+
this.#dom = dom;
|
|
90
|
+
}
|
|
91
|
+
connectedCallback() {
|
|
92
|
+
this.#connected = true;
|
|
93
|
+
this.#updateSelectionChangeListener();
|
|
94
|
+
}
|
|
95
|
+
dispose() {
|
|
96
|
+
this.#connected = false;
|
|
97
|
+
this.#updateSelectionChangeListener();
|
|
98
|
+
}
|
|
99
|
+
_handleEnableSelectionChangeEvent(status) {
|
|
100
|
+
this.#enabled = status;
|
|
101
|
+
if (!status)
|
|
102
|
+
this.#lastSelectionSignature = undefined;
|
|
103
|
+
this.#updateSelectionChangeListener();
|
|
104
|
+
}
|
|
105
|
+
#updateSelectionChangeListener() {
|
|
106
|
+
const document = this.#dom.ownerDocument;
|
|
107
|
+
const shouldAttach = this.#enabled && this.#connected;
|
|
108
|
+
if (shouldAttach && this.#attachedDocument === document)
|
|
109
|
+
return;
|
|
110
|
+
if (!shouldAttach && !this.#attachedDocument)
|
|
111
|
+
return;
|
|
112
|
+
this.#attachedDocument?.removeEventListener('selectionchange', this.#handleSelectionChange);
|
|
113
|
+
if (shouldAttach) {
|
|
114
|
+
document.addEventListener('selectionchange', this.#handleSelectionChange);
|
|
115
|
+
}
|
|
116
|
+
this.#attachedDocument = shouldAttach ? document : undefined;
|
|
117
|
+
}
|
|
118
|
+
#handleSelectionChange = () => {
|
|
119
|
+
const detail = getSelectionDetail(this.#dom);
|
|
120
|
+
const signature = `${detail.start}:${detail.end}:${detail.direction}`;
|
|
121
|
+
if (signature === this.#lastSelectionSignature
|
|
122
|
+
|| (detail.start === -1 && this.#lastSelectionSignature === undefined)) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
this.#lastSelectionSignature = signature;
|
|
126
|
+
this.#dom.dispatchEvent(new CustomEvent('selectionchange', {
|
|
127
|
+
...commonComponentEventSetting,
|
|
128
|
+
detail,
|
|
129
|
+
}));
|
|
130
|
+
};
|
|
131
|
+
};
|
|
132
|
+
})();
|
|
133
|
+
export { XTextSelectionEvents };
|
|
134
|
+
//# sourceMappingURL=XTextSelectionEvents.js.map
|
|
@@ -14,6 +14,7 @@ export { RawText } from './RawText.js';
|
|
|
14
14
|
*
|
|
15
15
|
* Events:
|
|
16
16
|
* - `layout`: Fired when text layout happens (if enabled). Detail provides line info.
|
|
17
|
+
* - `selectionchange`: Fired when the text selection changes. Detail provides start, end, and direction.
|
|
17
18
|
*
|
|
18
19
|
* CSS Variables:
|
|
19
20
|
* - `--lynx-text-bg-color`: Inherited background color for nested text elements.
|
|
@@ -17,6 +17,7 @@ export { RawText } from './RawText.js';
|
|
|
17
17
|
*
|
|
18
18
|
* Events:
|
|
19
19
|
* - `layout`: Fired when text layout happens (if enabled). Detail provides line info.
|
|
20
|
+
* - `selectionchange`: Fired when the text selection changes. Detail provides start, end, and direction.
|
|
20
21
|
*
|
|
21
22
|
* CSS Variables:
|
|
22
23
|
* - `--lynx-text-bg-color`: Inherited background color for nested text elements.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lynx-js/web-elements",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.10",
|
|
4
4
|
"private": false,
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -34,6 +34,11 @@
|
|
|
34
34
|
"types": "./dist/elements/ScrollView/index.d.ts",
|
|
35
35
|
"default": "./dist/elements/ScrollView/index.js"
|
|
36
36
|
},
|
|
37
|
+
"./plugins/scroll-view-mouse-drag": {
|
|
38
|
+
"@lynx-js/source-field": "./src/elements/ScrollView/ScrollViewMouseDrag.ts",
|
|
39
|
+
"types": "./dist/elements/ScrollView/ScrollViewMouseDrag.d.ts",
|
|
40
|
+
"default": "./dist/elements/ScrollView/ScrollViewMouseDrag.js"
|
|
41
|
+
},
|
|
37
42
|
"./XAudioTT": {
|
|
38
43
|
"@lynx-js/source-field": "./src/elements/XAudioTT/index.ts",
|
|
39
44
|
"types": "./dist/elements/XAudioTT/index.d.ts",
|
|
@@ -135,11 +140,11 @@
|
|
|
135
140
|
],
|
|
136
141
|
"dependencies": {
|
|
137
142
|
"dompurify": "^3.4.13",
|
|
138
|
-
"markdown-it": "^15.0.
|
|
143
|
+
"markdown-it": "^15.0.1"
|
|
139
144
|
},
|
|
140
145
|
"devDependencies": {
|
|
141
146
|
"@playwright/test": "^1.61.1",
|
|
142
|
-
"@rsbuild/core": "2.
|
|
147
|
+
"@rsbuild/core": "2.2.3",
|
|
143
148
|
"@rsbuild/plugin-source-build": "1.0.6",
|
|
144
149
|
"@types/markdown-it": "^14.1.2",
|
|
145
150
|
"@types/node": "^24.13.3",
|