/
focusableElement.js
189 lines (156 loc) · 4.49 KB
/
focusableElement.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
import EventManager from './../../eventManager';
import localHooks from './../../mixins/localHooks';
import { mixin } from './../../helpers/object';
import { isMobileBrowser } from './../../helpers/browser';
import { selectElementIfAllowed } from './../../helpers/dom/element';
/**
* @class FocusableWrapper
*
* @plugin CopyPaste
*/
class FocusableWrapper {
constructor(rootDocument) {
this.rootDocument = rootDocument;
/**
* The main/operational focusable element.
*
* @type {HTMLElement}
*/
this.mainElement = null;
/**
* Instance of EventManager.
*
* @type {EventManager}
*/
this.eventManager = new EventManager(this);
/**
* An object for tracking information about event listeners attached to the focusable element.
*
* @type {WeakSet}
*/
this.listenersCount = new WeakSet();
}
/**
* Switch to the secondary focusable element. Used when no any main focusable element is provided.
*/
useSecondaryElement() {
const el = createOrGetSecondaryElement(this.rootDocument);
if (!this.listenersCount.has(el)) {
this.listenersCount.add(el);
forwardEventsToLocalHooks(this.eventManager, el, this);
}
this.mainElement = el;
}
/**
* Switch to the main focusable element.
*
* @param {HTMLElement} element
*/
setFocusableElement(element) {
if (!this.listenersCount.has(element)) {
this.listenersCount.add(element);
forwardEventsToLocalHooks(this.eventManager, element, this);
}
this.mainElement = element;
}
/**
* Get currently set focusable element.
*
* @returns {HTMLElement}
*/
getFocusableElement() {
return this.mainElement;
}
/**
* Set focus to the focusable element.
*/
focus() {
// Add an empty space to texarea. It is necessary for safari to enable "copy" command from menu bar.
this.mainElement.value = ' ';
if (!isMobileBrowser()) {
selectElementIfAllowed(this.mainElement);
}
}
}
mixin(FocusableWrapper, localHooks);
let refCounter = 0;
/**
* Create and return the FocusableWrapper instance.
*
* @returns {FocusableWrapper}
*/
function createElement(rootDocument) {
const focusableWrapper = new FocusableWrapper(rootDocument);
refCounter += 1;
return focusableWrapper;
}
/**
* Deactivate the FocusableWrapper instance.
*
* @param {FocusableWrapper} wrapper
*/
function deactivateElement(wrapper) {
wrapper.eventManager.clear();
}
const runLocalHooks = (eventName, subject) => event => subject.runLocalHooks(eventName, event);
/**
* Register copy/cut/paste events and forward their actions to the subject local hooks system.
*
* @param {HTMLElement} element
* @param {FocusableWrapper} subject
*/
function forwardEventsToLocalHooks(eventManager, element, subject) {
eventManager.addEventListener(element, 'copy', runLocalHooks('copy', subject));
eventManager.addEventListener(element, 'cut', runLocalHooks('cut', subject));
eventManager.addEventListener(element, 'paste', runLocalHooks('paste', subject));
}
const secondaryElements = new WeakMap();
/**
* Create and attach newly created focusable element to the DOM.
*
* @returns {HTMLElement}
*/
function createOrGetSecondaryElement(rootDocument) {
const secondaryElement = secondaryElements.get(rootDocument);
if (secondaryElement) {
if (!secondaryElement.parentElement) {
this.rootDocument.body.appendChild(secondaryElement);
}
return secondaryElement;
}
const element = rootDocument.createElement('textarea');
secondaryElements.set(rootDocument, element);
element.id = 'HandsontableCopyPaste';
element.className = 'copyPaste';
element.tabIndex = -1;
element.autocomplete = 'off';
element.wrap = 'hard';
element.value = ' ';
rootDocument.body.appendChild(element);
return element;
}
/**
* Destroy the FocusableWrapper instance.
*
* @param {FocusableWrapper} wrapper
*/
function destroyElement(wrapper) {
if (!(wrapper instanceof FocusableWrapper)) {
return;
}
if (refCounter > 0) {
refCounter -= 1;
}
deactivateElement(wrapper);
if (refCounter <= 0) {
refCounter = 0;
// Detach secondary element from the DOM.
const secondaryElement = secondaryElements.get(wrapper.rootDocument);
if (secondaryElement && secondaryElement.parentNode) {
secondaryElement.parentNode.removeChild(secondaryElement);
secondaryElements.delete(wrapper.rootDocument);
}
wrapper.mainElement = null;
}
}
export { createElement, deactivateElement, destroyElement };