bm-core-ui 2.7.0
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 +1624 -0
- package/LICENSE +21 -0
- package/README.md +145 -0
- package/build/@types/index.d.ts +14078 -0
- package/build/BMCodeEditor/BMCodeEditor.js +707 -0
- package/build/BMCollectionView/BMCollectionView.js +5861 -0
- package/build/BMCollectionView/BMCollectionViewCell.js +688 -0
- package/build/BMCollectionView/BMCollectionViewFlowLayout.js +4467 -0
- package/build/BMCollectionView/BMCollectionViewLayout.js +830 -0
- package/build/BMCollectionView/BMCollectionViewLayoutAttributes.js +673 -0
- package/build/BMCollectionView/BMCollectionViewMasonryLayout.js +491 -0
- package/build/BMCollectionView/BMCollectionViewStackLayout.js +634 -0
- package/build/BMCollectionView/BMCollectionViewTileLayout.js +1121 -0
- package/build/BMCoreUI.css +2833 -0
- package/build/BMView/BMAttributedLabelView.js +310 -0
- package/build/BMView/BMLayoutConstraint_v2.5.js +1813 -0
- package/build/BMView/BMLayoutGuide.js +192 -0
- package/build/BMView/BMLayoutSizeClass.js +466 -0
- package/build/BMView/BMMenu.js +574 -0
- package/build/BMView/BMScrollView.js +247 -0
- package/build/BMView/BMTextField.js +512 -0
- package/build/BMView/BMTextFieldDelegate.js +71 -0
- package/build/BMView/BMView_v2.5.js +3572 -0
- package/build/BMView/BMViewport.js +221 -0
- package/build/BMViewLayoutEditor/BMLayoutEditor.js +6117 -0
- package/build/BMViewLayoutEditor/BMLayoutEditorConstraintSettings.js +417 -0
- package/build/BMViewLayoutEditor/BMLayoutEditorDelegate.js +59 -0
- package/build/BMViewLayoutEditor/BMLayoutEditorSettingCells.js +1653 -0
- package/build/BMViewLayoutEditor/BMLayoutEditorSettings.js +1480 -0
- package/build/BMViewLayoutEditor/BMLayoutEditorSettingsComplexCells.js +431 -0
- package/build/BMViewLayoutEditor/BMLayoutEditorSettingsDelegate.js +46 -0
- package/build/BMViewLayoutEditor/BMLayoutEditorVariablesController.js +460 -0
- package/build/BMViewLayoutEditor/BMLayoutEditorViewGroupSettings.js +293 -0
- package/build/BMViewLayoutEditor/BMLayoutEditorViewSettings.js +382 -0
- package/build/BMViewLayoutEditor/BMLayoutVariableProvider.js +202 -0
- package/build/BMWindow/BMConfirmationPopup.js +477 -0
- package/build/BMWindow/BMKeyboardShortcut.js +151 -0
- package/build/BMWindow/BMPopover/BMPopover.js +492 -0
- package/build/BMWindow/BMToolWindow.js +71 -0
- package/build/BMWindow/BMWindow.js +2000 -0
- package/build/Core/BMAnimationContext.js +1181 -0
- package/build/Core/BMColor.js +991 -0
- package/build/Core/BMCoreUI.js +470 -0
- package/build/Core/BMFunctionCollection.js +110 -0
- package/build/Core/BMIndexPath.js +165 -0
- package/build/Core/BMInset.js +138 -0
- package/build/Core/BMKeyPath.js +100 -0
- package/build/Core/BMPoint.js +291 -0
- package/build/Core/BMRect.js +556 -0
- package/build/Core/BMSize.js +137 -0
- package/build/iScroll/LICENSE +22 -0
- package/build/iScroll/iscroll-probe.js +2154 -0
- package/build/images/AlignBottom.png +0 -0
- package/build/images/AlignCenterX.png +0 -0
- package/build/images/AlignCenterY.png +0 -0
- package/build/images/AlignLeading.png +0 -0
- package/build/images/AlignTop.png +0 -0
- package/build/images/AlignTrailing.png +0 -0
- package/build/images/AllConstraints.png +0 -0
- package/build/images/BottomConstraint.png +0 -0
- package/build/images/CenterXConstraint.png +0 -0
- package/build/images/CenterYConstraint.png +0 -0
- package/build/images/CoreUI2.png +0 -0
- package/build/images/CoreUI2@2x.png +0 -0
- package/build/images/Desktop.png +0 -0
- package/build/images/DesktopMini.png +0 -0
- package/build/images/EqualHeight.png +0 -0
- package/build/images/EqualHorizontalSpacing.png +0 -0
- package/build/images/EqualHorizontalSpacingInSuperview.png +0 -0
- package/build/images/EqualVerticalSpacing.png +0 -0
- package/build/images/EqualVerticalSpacingInSuperview.png +0 -0
- package/build/images/EqualWidth.png +0 -0
- package/build/images/HeightConstraint.png +0 -0
- package/build/images/InactiveConstraints.png +0 -0
- package/build/images/Layout.png +0 -0
- package/build/images/LayoutVariables.png +0 -0
- package/build/images/LeftConstraint.png +0 -0
- package/build/images/OwnConstraints.png +0 -0
- package/build/images/Phone.png +0 -0
- package/build/images/PhoneLandscape.png +0 -0
- package/build/images/PhoneLandscapeMini.png +0 -0
- package/build/images/PhoneMini.png +0 -0
- package/build/images/PhonePortrait.png +0 -0
- package/build/images/PhonePortraitMini.png +0 -0
- package/build/images/Properties.png +0 -0
- package/build/images/RightConstraint.png +0 -0
- package/build/images/SubviewConstraints.png +0 -0
- package/build/images/Tablet.png +0 -0
- package/build/images/TabletLandscape.png +0 -0
- package/build/images/TabletLandscapeMini.png +0 -0
- package/build/images/TabletMini.png +0 -0
- package/build/images/TabletPortrait.png +0 -0
- package/build/images/TabletPortraitMini.png +0 -0
- package/build/images/TopConstraint.png +0 -0
- package/build/images/WidthConstraint.png +0 -0
- package/build/index.js +40 -0
- package/lib/@types/BMCoreUI.min.d.ts +14078 -0
- package/lib/BMCoreUI.min.js +1 -0
- package/package.json +58 -0
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
import { BMExtend, NO } from "../Core/BMCoreUI";
|
|
2
|
+
import { BMView } from "./BMView_v2.5";
|
|
3
|
+
import { BMMenuKind } from "./BMMenu";
|
|
4
|
+
|
|
5
|
+
// @ts-check
|
|
6
|
+
|
|
7
|
+
const BM_TEXT_FIELD_MAX_SUGGESTIONS = 10;
|
|
8
|
+
|
|
9
|
+
// @type BMTextField extends BMView
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A text field is a vew wrapper around an `input` type `"text"` element that adds additional
|
|
13
|
+
* functionalities such as a drop-down with suggestions and auto completion.
|
|
14
|
+
*/
|
|
15
|
+
export function BMTextField() {} // <constructor>
|
|
16
|
+
|
|
17
|
+
BMTextField.prototype = BMExtend(Object.create(BMView.prototype), {
|
|
18
|
+
constructor: BMTextField,
|
|
19
|
+
|
|
20
|
+
// @override - BMView
|
|
21
|
+
supportsAutomaticIntrinsicSize: NO,
|
|
22
|
+
|
|
23
|
+
// @override - BMView
|
|
24
|
+
supportsIntrinsicSize: NO,
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Used to control whether this text field autocompletes based on suggestions obtained from its delegate object.
|
|
28
|
+
* When this property is set to `YES`, whenever the text that the user is typing matches one of the suggestions
|
|
29
|
+
* the rest of the text will be automatically filled in and selected.
|
|
30
|
+
*
|
|
31
|
+
* When this property is set to `NO`, suggestions will still appear as a drop down menu, but the text will not be automatically completed.
|
|
32
|
+
*/
|
|
33
|
+
_autoCompletes: YES, // <Boolean>
|
|
34
|
+
get autoCompletes() {
|
|
35
|
+
return this._autoCompletes;
|
|
36
|
+
},
|
|
37
|
+
set autoCompletes(completes) {
|
|
38
|
+
this._autoCompletes = completes;
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Used to control whether this text field supports suggestions that appear for the user as they are typing in this text field.
|
|
43
|
+
* When this property is set to `YES`, if there are any suggestions obtained from the delegate object, the matching ones will appear
|
|
44
|
+
* as a drop down below the text field. The text field will automatically select the first matching suggestion and fill it in if the user
|
|
45
|
+
* presses the `return` key.
|
|
46
|
+
*
|
|
47
|
+
* When this property is set to `NO`, suggestions will be unavailable. This will also disable auto completion.
|
|
48
|
+
*/
|
|
49
|
+
_usesSuggestions: YES, // <Boolean>
|
|
50
|
+
get usesSuggestions() {
|
|
51
|
+
return this._usesSuggestions;
|
|
52
|
+
},
|
|
53
|
+
set usesSuggestions(uses) {
|
|
54
|
+
if (uses != this._usesSuggestions) {
|
|
55
|
+
this._usesSuggestions = uses;
|
|
56
|
+
|
|
57
|
+
if (uses) {
|
|
58
|
+
this._setUpEventHandlers();
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
this._clearEventHandlers();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* An optional delegate that can be used to respond to various callbacks from the text field.
|
|
68
|
+
*/
|
|
69
|
+
delegate: undefined, // <BMTextFieldDelegate, nullable>
|
|
70
|
+
|
|
71
|
+
// @override - BMView
|
|
72
|
+
initWithDOMNode(node) {
|
|
73
|
+
BMView.prototype.initWithDOMNode.apply(this, arguments);
|
|
74
|
+
|
|
75
|
+
if (this._usesSuggestions) this._setUpEventHandlers();
|
|
76
|
+
|
|
77
|
+
return this;
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The focus event listener.
|
|
82
|
+
*/
|
|
83
|
+
_focusHandler: undefined, // <Promise<void> ^(Event)>
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The focus event listener.
|
|
87
|
+
*/
|
|
88
|
+
_blurHandler: undefined, // <void ^(Event)>
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The focus event listener.
|
|
92
|
+
*/
|
|
93
|
+
_keydownHandler: undefined, // <void ^(Event)>
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The focus event listener.
|
|
97
|
+
*/
|
|
98
|
+
_inputHandler: undefined, // <void ^(Event)>
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The paste event listener.
|
|
102
|
+
*/
|
|
103
|
+
_pasteHandler: undefined, // <void ^(Event)>
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Sets up the event handlers for this text field to handle suggestions.
|
|
107
|
+
*/
|
|
108
|
+
_setUpEventHandlers() {
|
|
109
|
+
/** @type {HTMLInputElement} */ const inputNode = this.node;
|
|
110
|
+
|
|
111
|
+
// This array contains all the available layout variables and is initialized upon the input
|
|
112
|
+
// element acquiring keyboard focus
|
|
113
|
+
let suggestions = [];
|
|
114
|
+
// This array contains the layout variables that match the text contained within the input element
|
|
115
|
+
let filteredSuggestions = [];
|
|
116
|
+
// The popup menu displaying the filtered suggestions
|
|
117
|
+
let menu;
|
|
118
|
+
// The index of the top hit suggestion. An index of -1 represents no top hit.
|
|
119
|
+
let highlightedItemIndex;
|
|
120
|
+
|
|
121
|
+
// TODO: Consider encapsulating this functionality into a separate auto-complete text box class
|
|
122
|
+
inputNode.addEventListener('focus', this._focusHandler = async event => {
|
|
123
|
+
// If suggestions are not enabled, no additional action is required
|
|
124
|
+
if (!this._usesSuggestions) return;
|
|
125
|
+
|
|
126
|
+
// TODO: Consider whether this is appropriate for this class
|
|
127
|
+
// Select all content upon receiving focus
|
|
128
|
+
// inputNode.select();
|
|
129
|
+
|
|
130
|
+
let options = [];
|
|
131
|
+
|
|
132
|
+
if (this.delegate && this.delegate.textFieldSuggestionsForText) {
|
|
133
|
+
suggestions = this.delegate.textFieldSuggestionsForText(this, this.value);
|
|
134
|
+
}
|
|
135
|
+
else return;
|
|
136
|
+
|
|
137
|
+
filteredSuggestions = suggestions;
|
|
138
|
+
|
|
139
|
+
highlightedItemIndex = -1;
|
|
140
|
+
|
|
141
|
+
let limit = BM_TEXT_FIELD_MAX_SUGGESTIONS;
|
|
142
|
+
for (const option of suggestions) {
|
|
143
|
+
options.push(this._menuItemWithLabel(option, {action: () => {
|
|
144
|
+
menu = undefined;
|
|
145
|
+
inputNode.value = option;
|
|
146
|
+
|
|
147
|
+
if (this.delegate && this.delegate.textFieldContentsDidChange) {
|
|
148
|
+
this.delegate.textFieldContentsDidChange(this);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Remove focus from the text box upon selecting a value
|
|
152
|
+
inputNode.blur();
|
|
153
|
+
}}));
|
|
154
|
+
|
|
155
|
+
// Limit to 10 items
|
|
156
|
+
limit--;
|
|
157
|
+
if (!limit) break;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Create and show a pulldown menu below the constant textbox, if there are any suggestions
|
|
161
|
+
let menuShouldDisplay = YES;
|
|
162
|
+
if (this.delegate.textFieldShouldShowSuggestions) {
|
|
163
|
+
menuShouldDisplay = this.delegate.textFieldShouldShowSuggestions(this);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (menuShouldDisplay && suggestions.length) {
|
|
167
|
+
let frame = BMRectMakeWithNodeFrame(inputNode);
|
|
168
|
+
let point = BMPointMake(frame.origin.x, frame.bottom);
|
|
169
|
+
menu = this._showMenuAtPoint(point, {withOptions: options, kind: BMMenuKind.PullDownMenu});
|
|
170
|
+
menu.classList.add('BMLayoutEditorConstantSuggestions');
|
|
171
|
+
|
|
172
|
+
menu.parentNode.classList.add('BMLayoutEditorConstantSuggestionsOverlay');
|
|
173
|
+
|
|
174
|
+
// Prevent the menu from stealing focus, which would dismiss it before an option is selected
|
|
175
|
+
menu.addEventListener('mousedown', e => e.preventDefault(), YES);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const fn = event => event.preventDefault();
|
|
179
|
+
inputNode.addEventListener('mouseup', fn);
|
|
180
|
+
|
|
181
|
+
// This timeout is necessary because otherwise regular clicks would instantly dismiss the menu
|
|
182
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
183
|
+
inputNode.removeEventListener('mouseup', fn);
|
|
184
|
+
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// The backspace and delete keys are not identifiable in input events
|
|
188
|
+
// So they are detected on keydown prior to the input event occurring
|
|
189
|
+
let isBackspaceInput = NO;
|
|
190
|
+
|
|
191
|
+
inputNode.addEventListener('keydown', this._keydownHandler = event => {
|
|
192
|
+
// If suggestions are not enabled, no additional action is required
|
|
193
|
+
if (!this._usesSuggestions) return;
|
|
194
|
+
|
|
195
|
+
if (event.code == 'ArrowDown' || event.code == 'ArrowUp') {
|
|
196
|
+
event.preventDefault();
|
|
197
|
+
if (filteredSuggestions.length == 0) return;
|
|
198
|
+
|
|
199
|
+
// Move the selection accordingly
|
|
200
|
+
if (event.code == 'ArrowUp') {
|
|
201
|
+
highlightedItemIndex = Math.max((highlightedItemIndex || 0) - 1, 0);
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
highlightedItemIndex = Math.min((isNaN(highlightedItemIndex) ? -1 : highlightedItemIndex) + 1, filteredSuggestions.length - 1);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Move the menu highlight
|
|
208
|
+
if (menu) {
|
|
209
|
+
// Remove the highlighted class from the previously selected item
|
|
210
|
+
for (const element of menu.querySelectorAll('.BMLayoutEditorConstraintPopupOptionHighlighted')) {
|
|
211
|
+
element.classList.remove('BMLayoutEditorConstraintPopupOptionHighlighted');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Highlight the current item
|
|
215
|
+
menu.childNodes[highlightedItemIndex].classList.add('BMLayoutEditorConstraintPopupOptionHighlighted');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (event.code == 'Enter') {
|
|
221
|
+
// Allow the delegate to supress the default action
|
|
222
|
+
let shouldReturn = YES;
|
|
223
|
+
if (this.delegate && this.delegate.textFieldShouldReturn) {
|
|
224
|
+
shouldReturn = this.delegate.textFieldShouldReturn(this);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (typeof shouldReturn == 'undefined') shouldReturn = YES;
|
|
228
|
+
|
|
229
|
+
if (!shouldReturn) return;
|
|
230
|
+
|
|
231
|
+
// Upon pressing the enter key, select the currently highlighted item, if available
|
|
232
|
+
// then remove focus from the constant box
|
|
233
|
+
let performAutocomplete = NO;
|
|
234
|
+
if (!isNaN(highlightedItemIndex) && filteredSuggestions.length && filteredSuggestions[highlightedItemIndex]) {
|
|
235
|
+
if (this.delegate && this.delegate.textFieldShouldAutocompleteText) {
|
|
236
|
+
performAutocomplete = this.delegate.textFieldShouldAutocompleteText(this, inputNode.value, {withSuggestion: filteredSuggestions[highlightedItemIndex]});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (performAutocomplete) {
|
|
240
|
+
inputNode.value = filteredSuggestions[highlightedItemIndex];
|
|
241
|
+
|
|
242
|
+
if (this.delegate && this.delegate.textFieldContentsDidChange) {
|
|
243
|
+
this.delegate.textFieldContentsDidChange(this);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
inputNode.blur();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (event.code == 'Backspace' || event.code == 'Delete') {
|
|
252
|
+
isBackspaceInput = YES;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
inputNode.addEventListener('input', this._inputHandler = event => {
|
|
258
|
+
// If suggestions are not enabled, no additional action is required
|
|
259
|
+
if (!this._usesSuggestions) {
|
|
260
|
+
if (!textFieldDidHighlightSuggestion) {
|
|
261
|
+
if (this.delegate && this.delegate.textFieldContentsDidChange) {
|
|
262
|
+
this.delegate.textFieldContentsDidChange(this);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Request new suggestions from the delegate for the new text
|
|
268
|
+
if (this.delegate && this.delegate.textFieldSuggestionsForText) {
|
|
269
|
+
suggestions = this.delegate.textFieldSuggestionsForText(this, this.value);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// If the menu is visible, filter it accordingly
|
|
273
|
+
filteredSuggestions = suggestions.filter(e => e.toLowerCase().startsWith(inputNode.value.toLowerCase()));
|
|
274
|
+
|
|
275
|
+
// Limit to the topmost 10 items
|
|
276
|
+
filteredSuggestions.length = Math.min(filteredSuggestions.length, 10);
|
|
277
|
+
|
|
278
|
+
// Empty the menu
|
|
279
|
+
if (menu) menu.innerHTML = '';
|
|
280
|
+
|
|
281
|
+
// Reset the highlighted item index
|
|
282
|
+
highlightedItemIndex = 0;
|
|
283
|
+
|
|
284
|
+
// Then add the filtered options
|
|
285
|
+
let limit = BM_TEXT_FIELD_MAX_SUGGESTIONS;
|
|
286
|
+
if (menu) for (const option of filteredSuggestions) {
|
|
287
|
+
menu.appendChild(this._menuItemWithLabel(option, {action: () => {
|
|
288
|
+
menu = undefined;
|
|
289
|
+
inputNode.value = option;
|
|
290
|
+
|
|
291
|
+
if (this.delegate && this.delegate.textFieldContentsDidChange) {
|
|
292
|
+
this.delegate.textFieldContentsDidChange(this);
|
|
293
|
+
}
|
|
294
|
+
}}));
|
|
295
|
+
|
|
296
|
+
// Limit to 10 items
|
|
297
|
+
limit--;
|
|
298
|
+
if (!limit) break;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Set to YES if the change event is fired due to autocompleting the text in the text field
|
|
302
|
+
let textFieldDidHighlightSuggestion = NO;
|
|
303
|
+
|
|
304
|
+
// Highlight the first item
|
|
305
|
+
if (filteredSuggestions.length) {
|
|
306
|
+
if (menu) {
|
|
307
|
+
menu.childNodes[0].classList.add('BMLayoutEditorConstraintPopupOptionHighlighted');
|
|
308
|
+
menu.style.display = 'block';
|
|
309
|
+
|
|
310
|
+
// Need to reposition the menu if it appears above the text box as it may float at a distance
|
|
311
|
+
// due to items being removed
|
|
312
|
+
let frame = BMRectMakeWithNodeFrame(inputNode);
|
|
313
|
+
let point = BMPointMake(frame.origin.x, frame.bottom);
|
|
314
|
+
this._repositionMenuAtPoint(point);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Autocomplete the suggestion, if the caret is at the end of the text field
|
|
318
|
+
// and if this input isn't the result of pressing backspace or delete
|
|
319
|
+
if (this._autoCompletes && !isBackspaceInput) {
|
|
320
|
+
const selectionStart = inputNode.selectionStart;
|
|
321
|
+
if (selectionStart == inputNode.value.length) {
|
|
322
|
+
textFieldDidHighlightSuggestion = YES;
|
|
323
|
+
|
|
324
|
+
inputNode.value = filteredSuggestions[0];
|
|
325
|
+
// Select the remainder of the suggestion
|
|
326
|
+
inputNode.setSelectionRange(selectionStart, inputNode.value.length, "backward");
|
|
327
|
+
|
|
328
|
+
if (this.delegate && this.delegate.textFieldContentsDidChange) {
|
|
329
|
+
this.delegate.textFieldContentsDidChange(this);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Clear the backspace flag upon processing it once
|
|
335
|
+
if (isBackspaceInput) {
|
|
336
|
+
isBackspaceInput = NO;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
if (menu) menu.style.display = 'none';
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (!textFieldDidHighlightSuggestion) {
|
|
344
|
+
if (this.delegate && this.delegate.textFieldContentsDidChange) {
|
|
345
|
+
this.delegate.textFieldContentsDidChange(this);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// Upon the value box losing focus, dismiss the menu, if it was open
|
|
351
|
+
// unless the newly focused item is a descendant of the menu
|
|
352
|
+
inputNode.addEventListener('blur', this._blurHandler = event => {
|
|
353
|
+
if (menu) {
|
|
354
|
+
menu.parentNode.dispatchEvent(new Event('click'));
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
inputNode.addEventListener('paste', this._pasteHandler = event => {
|
|
359
|
+
if (this.delegate && this.delegate.textFieldContentsDidChange) {
|
|
360
|
+
this.delegate.textFieldContentsDidChange(this);
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
},
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Clears the event handlers for this text field when suggestions are disabled.
|
|
367
|
+
*/
|
|
368
|
+
_clearEventHandlers() {
|
|
369
|
+
this.node.removeEventListener('focus', this._focusHandler);
|
|
370
|
+
this.node.removeEventListener('blur', this._blurHandler);
|
|
371
|
+
this.node.removeEventListener('keydown', this._keydownHandler);
|
|
372
|
+
this.node.removeEventListener('input', this._inputHandler);
|
|
373
|
+
this.node.removeEventListener('input', this._pasteHandler);
|
|
374
|
+
},
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Constructs and returns a DOM node for a menu item with the given label.
|
|
378
|
+
* @param label <String> The text to display in the menu item.
|
|
379
|
+
* {
|
|
380
|
+
* @param action <void ^(Event)> An action that occurs when the menu item is selected.
|
|
381
|
+
* }
|
|
382
|
+
* @return <DOMNode> A DOM node to be added to a menu.
|
|
383
|
+
*/
|
|
384
|
+
_menuItemWithLabel(label, args) {
|
|
385
|
+
// TODO: Roll up into BMMenu
|
|
386
|
+
let option = document.createElement('div');
|
|
387
|
+
option.className = 'BMLayoutEditorConstraintPopupOption';
|
|
388
|
+
option.innerText = label;
|
|
389
|
+
|
|
390
|
+
option.addEventListener('click', args.action);
|
|
391
|
+
|
|
392
|
+
return option;
|
|
393
|
+
},
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Creates and brings up a context menu at the given coordinates, relative to the viewport.
|
|
399
|
+
* @param point <BMPoint> The point at which to show the menu.
|
|
400
|
+
* {
|
|
401
|
+
* @param withOptions <[DOMNode]> The options that the menu should display.
|
|
402
|
+
* @param kind <BMMenuKind, nullable> Defaults to .Menu. The kind of menu to show.
|
|
403
|
+
* }
|
|
404
|
+
* @return <DOMNode> The menu element.
|
|
405
|
+
*/
|
|
406
|
+
_showMenuAtPoint(point, args) {
|
|
407
|
+
// TODO: Roll up into BMMenu
|
|
408
|
+
let constraintPopup = document.createElement('div');
|
|
409
|
+
constraintPopup.className = 'BMLayoutEditorConstraintPopup BMTextFieldSuggestions';
|
|
410
|
+
|
|
411
|
+
menu = constraintPopup;
|
|
412
|
+
|
|
413
|
+
let constraintPopupContainer = document.createElement('div');
|
|
414
|
+
constraintPopupContainer.className = 'BMLayoutEditorConstraintPopupContainer';
|
|
415
|
+
if (!('backdropFilter' in document.body.style) && !('webkitBackdropFilter' in document.body.style)) {
|
|
416
|
+
constraintPopup.style.backgroundColor = 'white';
|
|
417
|
+
}
|
|
418
|
+
constraintPopupContainer.appendChild(constraintPopup);
|
|
419
|
+
|
|
420
|
+
args.withOptions.forEach(item => constraintPopup.appendChild(item));
|
|
421
|
+
|
|
422
|
+
BMHook(constraintPopup, {scaleX: args.kind == BMMenuKind.PullDownMenu ? 1 : .75, scaleY: .75, opacity: 0});
|
|
423
|
+
|
|
424
|
+
document.body.appendChild(constraintPopupContainer);
|
|
425
|
+
|
|
426
|
+
this._repositionMenuAtPoint(point);
|
|
427
|
+
|
|
428
|
+
constraintPopup.addEventListener('click', event => event.preventDefault());
|
|
429
|
+
|
|
430
|
+
(window.Velocity || $.Velocity).animate(constraintPopup, {scaleX: 1, scaleY: 1, opacity: 1, translateZ: 0}, {
|
|
431
|
+
duration: 200,
|
|
432
|
+
easing: 'easeOutQuad',
|
|
433
|
+
complete: _ => ((constraintPopup.style.pointerEvents = 'all'), constraintPopupContainer.style.pointerEvents = 'all')
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
let delay = 0;
|
|
437
|
+
for (let child of constraintPopup.childNodes) {
|
|
438
|
+
BMHook(child, {translateY: '16px', translateZ: 0, opacity: 0});
|
|
439
|
+
(window.Velocity || $.Velocity).animate(child, {translateY: '0px', translateZ: 0, opacity: 1}, {
|
|
440
|
+
duration: 100,
|
|
441
|
+
easing: 'easeOutQuad',
|
|
442
|
+
delay: delay
|
|
443
|
+
});
|
|
444
|
+
delay += 16;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
constraintPopupContainer.addEventListener('click', event => {
|
|
448
|
+
constraintPopup.style.pointerEvents = 'none';
|
|
449
|
+
constraintPopupContainer.style.pointerEvents = 'none';
|
|
450
|
+
let delay = constraintPopup.childNodes.length * 16 + 100 - 200;
|
|
451
|
+
delay = (delay < 0 ? 0 : delay);
|
|
452
|
+
|
|
453
|
+
(window.Velocity || $.Velocity).animate(constraintPopup, {scaleX: args.kind == BMMenuKind.PullDownMenu ? 1 : .75, scaleY: .75, opacity: 0, translateZ: 0}, {
|
|
454
|
+
duration: 200,
|
|
455
|
+
easing: 'easeInQuad',
|
|
456
|
+
delay: delay,
|
|
457
|
+
complete: _ => constraintPopupContainer.remove()
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
delay = 0;
|
|
461
|
+
for (let i = constraintPopup.childNodes.length - 1; i >= 0; i--) {
|
|
462
|
+
let child = constraintPopup.childNodes[i];
|
|
463
|
+
(window.Velocity || $.Velocity).animate(child, {translateY: '16px', translateZ: 0, opacity: 0}, {
|
|
464
|
+
duration: 100,
|
|
465
|
+
easing: 'easeInQuad',
|
|
466
|
+
delay: delay
|
|
467
|
+
});
|
|
468
|
+
delay += 16;
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
return constraintPopup;
|
|
473
|
+
},
|
|
474
|
+
|
|
475
|
+
_repositionMenuAtPoint(point) {
|
|
476
|
+
let height = menu.offsetHeight;
|
|
477
|
+
|
|
478
|
+
menu.style.transformOrigin = '50% 0%';
|
|
479
|
+
if (height + point.y > document.documentElement.clientHeight) {
|
|
480
|
+
menu.style.transformOrigin = '50% 100%';
|
|
481
|
+
point.y -= height + this.frame.size.height;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
menu.style.left = point.x + 'px';
|
|
485
|
+
menu.style.top = point.y + 'px';
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Constructs and returns a text field with a new DOM node.
|
|
492
|
+
* @return <BMTextField> A text field.
|
|
493
|
+
*/
|
|
494
|
+
BMTextField.textField = function () {
|
|
495
|
+
const node = document.createElement('input');
|
|
496
|
+
node.type = 'text';
|
|
497
|
+
|
|
498
|
+
const textField = Object.create(BMTextField.prototype).initWithDOMNode(node);
|
|
499
|
+
|
|
500
|
+
return textField;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Constructs and returns a text field for the given input DOM node.
|
|
505
|
+
* @param node <DOMNode> An input DOM node.
|
|
506
|
+
* @return <BMTextField> A text field.
|
|
507
|
+
*/
|
|
508
|
+
BMTextField.textFieldForInputNode = function (node) {
|
|
509
|
+
return Object.create(BMTextField.prototype).initWithDOMNode(node);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// @endtype
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// @type interface BMTextFieldDelegate
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The specification for a `BMTextField` object, which can optionally be used in conjunction with `BMTextField` objects and will receive various
|
|
5
|
+
* callbacks related to the text field's lifecycle.
|
|
6
|
+
*/
|
|
7
|
+
function BMTextFieldDelegate() {} // <constructor>
|
|
8
|
+
|
|
9
|
+
BMTextFieldDelegate.prototype = {
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Invoked by text fields to retrieve the available suggestions for autocomplete for the given text.
|
|
13
|
+
* This method should return the full list of suggestions available for the given text. The text field will further filter out the suggestions
|
|
14
|
+
* based on the text that the user has typed.
|
|
15
|
+
*
|
|
16
|
+
* If this method is not implemented, suggestions and autocomplete will not be available.
|
|
17
|
+
* @param textField <BMTextField> The calling text field.
|
|
18
|
+
* @param text <String>
|
|
19
|
+
* @return <[String]> An array of string suggestions.
|
|
20
|
+
*/
|
|
21
|
+
textFieldSuggestionsForText(textField, text) {},
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Invoked whenever the text field is about to show the suggestions drop down, if suggestions are available.
|
|
25
|
+
* Delegate objects can implement this method to control whether the drop down should be displayed.
|
|
26
|
+
*
|
|
27
|
+
* If this method is not implemented, the suggestions drop down will be displayed whenever appropriate.
|
|
28
|
+
* @param textField <BMTextField> The calling text field.
|
|
29
|
+
* @return <Boolean> `YES` if the suggestions drop down should appear, `NO` otherwise.
|
|
30
|
+
*/
|
|
31
|
+
textFieldShouldShowSuggestions(textField) {},
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Invoked whenever the text field is about to autocomplete the text with the given suggestion. This occurs when the
|
|
35
|
+
* user presses the return key.
|
|
36
|
+
* Delegate objects can implement this method to control whether the autocomplete should be performed.
|
|
37
|
+
*
|
|
38
|
+
* If autocomplete is enabled and this method is not implemented, the autocomplete will not be performed.
|
|
39
|
+
* @param textField <BMTextField> The calling text field.
|
|
40
|
+
* @param text <String> The text that has been typed in this text field.
|
|
41
|
+
* {
|
|
42
|
+
* @param withSuggestion <String> The suggestion
|
|
43
|
+
* }
|
|
44
|
+
*/
|
|
45
|
+
textFieldShouldAutocompleteText(textField, text, {withSuggestion}) {},
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Invoked whenever the contents in this text field change for any reason. This can be because of an input event,
|
|
49
|
+
* the user pasting text or a suggestion being selected.
|
|
50
|
+
* Note that this method will not be invoked when manually setting the input node's `value`.
|
|
51
|
+
*
|
|
52
|
+
* Delegate objects can implement this method in place of standard event listeners to handle changes to this text field.
|
|
53
|
+
* The contents of the text field can be retrieved via the standard `value` property on the text field's node.
|
|
54
|
+
* @param textField <BMTextField> The calling text field.
|
|
55
|
+
*/
|
|
56
|
+
textFieldContentsDidChange(textField) {},
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Invoked whenever the user presses the return key while the text field has keyboard focus.
|
|
60
|
+
*
|
|
61
|
+
* Delegate objects can implement this method to customize the behaviour when the user presses the return key and also control whether the default action will take place.
|
|
62
|
+
* The default action for pressing the return key is to cause the text field to resign keyboard focus. If suggestions are used, the return key will also fill in the currently
|
|
63
|
+
* highlighted suggestion.
|
|
64
|
+
* @param textField <BMTextField> The calling text field.
|
|
65
|
+
* @return <Boolean, nullable> Defaults to `YES`. If set to `YES`, the standard behaviour will be invoked.
|
|
66
|
+
*/
|
|
67
|
+
textFieldShouldReturn() {}
|
|
68
|
+
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// @endtype
|