Skip to content

Commit 53cb41a

Browse files
feat(addon/components/paper-select): converts to a glimmer component.
1 parent 3d10eeb commit 53cb41a

File tree

4 files changed

+385
-241
lines changed

4 files changed

+385
-241
lines changed

addon/components/paper-select.hbs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
{{! template-lint-disable no-action }}
2+
<md-input-container
3+
class='{{@class}}
4+
{{~if @selected " md-input-has-value"}}
5+
{{~if this.validation.isInvalidAndTouched " md-input-invalid"}}
6+
{{~if this.isFocusedAndSelected " md-input-focused"}}'
7+
{{did-insert this.didInsertNode}}
8+
{{did-update this.didUpdateNode @selected}}
9+
...attributes
10+
>
11+
12+
<PowerSelect
13+
@selected={{@selected}}
14+
@options={{@options}}
15+
@onChange={{@onChange}}
16+
@disabled={{@disabled}}
17+
@placeholder={{@placeholder}}
18+
@search={{@search}}
19+
@searchEnabled={{@searchEnabled}}
20+
@searchField={{@searchField}}
21+
@searchPlaceholder={{@searchPlaceholder}}
22+
@registerAPI={{action (mut this.publicAPI)}}
23+
@animationEnabled={{false}}
24+
@calculatePosition={{this.calculatePosition}}
25+
@extra={{hash label=@label}}
26+
@ebdTriggerComponent={{component
27+
'paper-select-ebd-trigger'
28+
label=@label
29+
selected=@selected
30+
required=@required
31+
disabled=@disabled
32+
}}
33+
@triggerComponent='paper-select-eps-trigger'
34+
@triggerClass='{{if this.validation.isInvalid "ng-invalid"}} {{if
35+
this.validation.isTouched
36+
"ng-dirty"
37+
}}'
38+
@ebdContentComponent={{component
39+
'paper-select-ebd-content'
40+
searchEnabled=@searchEnabled
41+
select=this.publicAPI
42+
}}
43+
@optionsComponent='paper-select-options'
44+
@beforeOptionsComponent='paper-select-search'
45+
@noMatchesMessageComponent='paper-select-no-matches-message'
46+
@searchMessageComponent='paper-select-search-message'
47+
@onClose={{this.handleClose}}
48+
@onOpen={{this.handleOpen}}
49+
@onFocus={{this.handleFocus}}
50+
@onBlur={{this.handleBlur}}
51+
as |opt term|
52+
>
53+
{{yield opt term}}
54+
</PowerSelect>
55+
56+
</md-input-container>

addon/components/paper-select.js

Lines changed: 329 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
1+
/**
2+
* @module ember-paper
3+
*/
4+
import Component from '@glimmer/component';
5+
import { tracked } from '@glimmer/tracking';
6+
import { action } from '@ember/object';
7+
import { guidFor } from '@ember/object/internals';
8+
import { assert } from '@ember/debug';
9+
import Validation from 'ember-paper/lib/validation';
10+
import clamp from 'ember-paper/utils/clamp';
11+
12+
const SELECT_EDGE_MARGIN = 8;
13+
14+
function getOffsetRect(node) {
15+
return node
16+
? {
17+
left: node.offsetLeft,
18+
top: node.offsetTop,
19+
width: node.offsetWidth,
20+
height: node.offsetHeight,
21+
}
22+
: { left: 0, top: 0, width: 0, height: 0 };
23+
}
24+
25+
/**
26+
* @class PaperSelect
27+
* @extends Component
28+
*/
29+
export default class PaperSelect extends Component {
30+
/**
31+
* tracks the validity of an input.
32+
*
33+
* @type {Validation}
34+
* @private
35+
*/
36+
validation;
37+
38+
/**
39+
* The parent this component is bound to.
40+
*
41+
* @type {PaperRadioGroup|PaperForm|PaperItem|PaperTabs}
42+
* @private
43+
*/
44+
#parent;
45+
/**
46+
* Marks whether the component should register itself to the supplied parent.
47+
*
48+
* @type {Boolean}
49+
* @public
50+
*/
51+
shouldRegister;
52+
53+
/**
54+
* true after having animated scaling out the element.
55+
*
56+
* @type {boolean}
57+
*/
58+
@tracked didAnimateScale;
59+
/**
60+
* true when the element is currently focused.
61+
*
62+
* @type {boolean}
63+
*/
64+
@tracked isFocused;
65+
66+
constructor(owner, args) {
67+
super(owner, args);
68+
69+
this.didAnimateScale = false;
70+
this.isFocused = false;
71+
72+
const elementId =
73+
this.args.elementId || this.args.inputElementId || guidFor(this);
74+
75+
// Construct Input Validation and pass through of custom attributes.
76+
this.validation = new Validation(
77+
elementId,
78+
this.args.onValidityChange || null,
79+
this.args.validations,
80+
this.args.customValidations,
81+
this.args.errors,
82+
this.args.errorMessages,
83+
this.args.isTouched
84+
);
85+
86+
if (this.shouldRegister) {
87+
assert(
88+
'A parent component should be supplied to <PaperInput> when shouldRegister=true',
89+
this.args.parentComponent
90+
);
91+
this.#parent = this.args.parentComponent;
92+
}
93+
94+
assert(
95+
'<PaperSelect> requires an `onChange` action or null for no action.',
96+
this.args.onChange !== undefined
97+
);
98+
}
99+
100+
/**
101+
* Performs any required DOM setup.
102+
*
103+
* @param {HTMLElement} element - the node that has been added to the DOM.
104+
*/
105+
@action didInsertNode() {
106+
// setValue ensures that the input value is the same as this.value
107+
this.validation.value = this.args.selected;
108+
109+
if (this.shouldRegister) {
110+
this.#parent.registerChild(this);
111+
}
112+
}
113+
114+
/**
115+
* didUpdateNode is called when tracked component attributes change.
116+
*/
117+
@action didUpdateNode() {
118+
// update validation checking
119+
if (this.args.errors) {
120+
this.validation.errors = this.args.errors;
121+
}
122+
123+
this.validation.value = this.args.selected;
124+
this.validation.validate(this.args);
125+
this.validation.notifyOnChange();
126+
}
127+
128+
/**
129+
* Performs any required DOM teardown.
130+
*
131+
* @param {HTMLElement} element - the node to be removed from the DOM.
132+
*/
133+
@action willDestroyNode() {
134+
// noop
135+
}
136+
137+
/**
138+
* lifecycle hook to perform non-DOM related teardown.
139+
*/
140+
willDestroy() {
141+
super.willDestroy(...arguments);
142+
143+
if (this.shouldRegister) {
144+
this.#parent.unregisterChild(this);
145+
}
146+
}
147+
148+
get isFocusedAndSelected() {
149+
return this.isFocused && this.args.isSelected;
150+
}
151+
152+
@action handleClose() {
153+
this.didAnimateScale = false;
154+
155+
this.validation.isTouched = true;
156+
this.validation.value = this.args.selected;
157+
this.validation.validate(this.args);
158+
this.validation.notifyOnValidityChange();
159+
}
160+
161+
@action handleOpen() {
162+
this.didAnimateScale = false;
163+
164+
this.validation.notifyOnValidityChange();
165+
}
166+
167+
@action handleFocus() {
168+
this.isFocused = true;
169+
}
170+
171+
@action handleBlur() {
172+
this.isFocused = false;
173+
}
174+
175+
@action calculatePosition(trigger, content) {
176+
let opts = {
177+
target: trigger,
178+
parent: document.body,
179+
selectEl: content.querySelector('md-select-menu'),
180+
contentEl: content.querySelector('md-content'),
181+
};
182+
183+
let containerNode = content;
184+
let targetNode = opts.target.firstElementChild; // target the label
185+
let parentNode = opts.parent;
186+
let selectNode = opts.selectEl;
187+
let contentNode = opts.contentEl;
188+
let parentRect = parentNode.getBoundingClientRect();
189+
let targetRect = targetNode.getBoundingClientRect();
190+
let shouldOpenAroundTarget = false;
191+
let bounds = {
192+
left: parentRect.left + SELECT_EDGE_MARGIN,
193+
top: SELECT_EDGE_MARGIN,
194+
bottom: parentRect.height - SELECT_EDGE_MARGIN,
195+
right: parentRect.width - SELECT_EDGE_MARGIN,
196+
};
197+
let spaceAvailable = {
198+
top: targetRect.top - bounds.top,
199+
left: targetRect.left - bounds.left,
200+
right: bounds.right - (targetRect.left + targetRect.width),
201+
bottom: bounds.bottom - (targetRect.top + targetRect.height),
202+
};
203+
let maxWidth = parentRect.width - SELECT_EDGE_MARGIN * 2;
204+
let selectedNode = selectNode.querySelector('md-option[selected]');
205+
let optionNodes = selectNode.getElementsByTagName('md-option');
206+
let optgroupNodes = selectNode.getElementsByTagName('md-optgroup');
207+
208+
let centeredNode, left, top, transformOrigin;
209+
210+
// If a selected node, center around that
211+
if (selectedNode) {
212+
centeredNode = selectedNode;
213+
// If there are option groups, center around the first option group
214+
} else if (optgroupNodes.length) {
215+
centeredNode = optgroupNodes[0];
216+
// Otherwise, center around the first optionNode
217+
} else if (optionNodes.length) {
218+
centeredNode = optionNodes[0];
219+
// In case there are no options, center on whatever's in there... (eg progress indicator)
220+
} else {
221+
centeredNode = contentNode.firstElementChild || contentNode;
222+
}
223+
224+
if (contentNode.offsetWidth > maxWidth) {
225+
contentNode.style['max-width'] = `${maxWidth}px`;
226+
}
227+
if (shouldOpenAroundTarget) {
228+
contentNode.style['min-width'] = `${targetRect.width}px`;
229+
}
230+
231+
// Remove padding before we compute the position of the menu
232+
233+
let focusedNode = centeredNode;
234+
if ((focusedNode.tagName || '').toUpperCase() === 'MD-OPTGROUP') {
235+
focusedNode =
236+
optionNodes[0] || contentNode.firstElementChild || contentNode;
237+
centeredNode = focusedNode;
238+
}
239+
240+
// Get the selectMenuRect *after* max-width is possibly set above
241+
containerNode.style.display = 'block';
242+
let selectMenuRect = selectNode.getBoundingClientRect();
243+
let centeredRect = getOffsetRect(centeredNode);
244+
245+
if (centeredNode) {
246+
let centeredStyle = window.getComputedStyle(centeredNode);
247+
centeredRect.paddingLeft = parseInt(centeredStyle.paddingLeft, 10) || 0;
248+
centeredRect.paddingRight = parseInt(centeredStyle.paddingRight, 10) || 0;
249+
}
250+
251+
// Get scrollHeight/offsetHeight *after* container is set with display:block
252+
let isScrollable = contentNode.scrollHeight > contentNode.offsetHeight;
253+
if (isScrollable) {
254+
let scrollBuffer = contentNode.offsetHeight / 2;
255+
contentNode.scrollTop =
256+
centeredRect.top + centeredRect.height / 2 - scrollBuffer;
257+
258+
if (spaceAvailable.top < scrollBuffer) {
259+
contentNode.scrollTop = Math.min(
260+
centeredRect.top,
261+
contentNode.scrollTop + scrollBuffer - spaceAvailable.top
262+
);
263+
} else if (spaceAvailable.bottom < scrollBuffer) {
264+
contentNode.scrollTop = Math.max(
265+
centeredRect.top + centeredRect.height - selectMenuRect.height,
266+
contentNode.scrollTop - scrollBuffer + spaceAvailable.bottom
267+
);
268+
}
269+
}
270+
271+
if (shouldOpenAroundTarget) {
272+
left = targetRect.left;
273+
top = targetRect.top + targetRect.height;
274+
transformOrigin = '50% 0';
275+
if (top + selectMenuRect.height > bounds.bottom) {
276+
top = targetRect.top - selectMenuRect.height;
277+
transformOrigin = '50% 100%';
278+
}
279+
} else {
280+
left = targetRect.left + centeredRect.left - centeredRect.paddingLeft + 2;
281+
top =
282+
Math.floor(
283+
targetRect.top +
284+
targetRect.height / 2 -
285+
centeredRect.height / 2 -
286+
centeredRect.top +
287+
contentNode.scrollTop
288+
) + 2;
289+
290+
transformOrigin = `${centeredRect.left + targetRect.width / 2}px
291+
${
292+
centeredRect.top + centeredRect.height / 2 - contentNode.scrollTop
293+
}px 0px`;
294+
295+
containerNode.style.minWidth = `${
296+
targetRect.width + centeredRect.paddingLeft + centeredRect.paddingRight
297+
}px`;
298+
}
299+
300+
let containerRect = containerNode.getBoundingClientRect();
301+
302+
let dropdownTop = clamp(
303+
bounds.top,
304+
top,
305+
bounds.bottom - containerRect.height
306+
);
307+
let dropdownLeft = clamp(
308+
bounds.left,
309+
left,
310+
bounds.right - containerRect.width
311+
);
312+
313+
let scaleX = Math.min(targetRect.width / selectMenuRect.width, 1.0);
314+
let scaleY = Math.min(targetRect.height / selectMenuRect.height, 1.0);
315+
let style = {
316+
top: dropdownTop,
317+
left: dropdownLeft,
318+
// Animate a scale out if we aren't just repositioning
319+
transform: !this.didAnimateScale
320+
? `scale(${scaleX}, ${scaleY})`
321+
: undefined,
322+
'transform-origin': transformOrigin,
323+
};
324+
325+
this.didAnimateScale = true;
326+
327+
return { style, horizontalPosition: '', verticalPosition: '' };
328+
}
329+
}

0 commit comments

Comments
 (0)