forked from davidflanagan/jstdg7
-
Notifications
You must be signed in to change notification settings - Fork 0
/
search-box.js
182 lines (167 loc) · 8.4 KB
/
search-box.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
/**
* This class defines a custom HTML <search-box> element that displays an
* <input> text input field plus two icons or emoji. By default, it displays a
* magnifying glass emoji (indicating search) to the left of the text field
* and an X emoji (indicating cancel) to the right of the text field. It
* hides the border on the input field and displays a border around itself,
* creating the appearance that the two emoji are inside the input
* field. Similarly, when the internal input field is focused, the focus ring
* is displayed around the <search-box>.
*
* You can override the default icons by including <span> or <img> children
* of <search-box> with slot="left" and slot="right" attributes.
*
* <search-box> supports the normal HTML disabled and hidden attributes and
* also size and placeholder attributes, which have the same meaning for this
* element as they do for the <input> element.
*
* Input events from the internal <input> element bubble up and appear with
* their target field set to the <search-box> element.
*
* The element fires a "search" event with the detail property set to the
* current input string when the user clicks on the left emoji (the magnifying
* glass). The "search" event is also dispatched when the internal text field
* generates a "change" event (when the text has changed and the user types
* Return or Tab).
*
* The element fires a "clear" event when the user clicks on the right emoji
* (the X). If no handler calls preventDefault() on the event then the element
* clears the user's input once event dispatch is complete.
*
* Note that there are no onsearch and onclear properties or attributes:
* handlers for the "search" and "clear" events can only be registered with
* addEventListener().
*/
class SearchBox extends HTMLElement {
constructor() {
super(); // Invoke the superclass constructor; must be first.
// Create a shadow DOM tree and attach it to this element, setting
// the value of this.shadowRoot.
this.attachShadow({mode: "open"});
// Clone the template that defines the descendants and stylesheet for
// this custom component, and append that content to the shadow root.
this.shadowRoot.append(SearchBox.template.content.cloneNode(true));
// Get references to the important elements in the shadow DOM
this.input = this.shadowRoot.querySelector("#input");
let leftSlot = this.shadowRoot.querySelector('slot[name="left"]');
let rightSlot = this.shadowRoot.querySelector('slot[name="right"]');
// When the internal input field gets or loses focus, set or remove
// the "focused" attribute which will cause our internal stylesheet
// to display or hide a fake focus ring on the entire component. Note
// that the "blur" and "focus" events bubble and appear to originate
// from the <search-box>.
this.input.onfocus = () => { this.setAttribute("focused", ""); };
this.input.onblur = () => { this.removeAttribute("focused");};
// If the user clicks on the magnifying glass, trigger a "search"
// event. Also trigger it if the input field fires a "change"
// event. (The "change" event does not bubble out of the Shadow DOM.)
leftSlot.onclick = this.input.onchange = (event) => {
event.stopPropagation(); // Prevent click events from bubbling
if (this.disabled) return; // Do nothing when disabled
this.dispatchEvent(new CustomEvent("search", {
detail: this.input.value
}));
};
// If the user clicks on the X, trigger a "clear" event.
// If preventDefault() is not called on the event, clear the input.
rightSlot.onclick = (event) => {
event.stopPropagation(); // Don't let the click bubble up
if (this.disabled) return; // Don't do anything if disabled
let e = new CustomEvent("clear", { cancelable: true });
this.dispatchEvent(e);
if (!e.defaultPrevented) { // If the event was not "cancelled"
this.input.value = ""; // then clear the input field
}
};
}
// When some of our attributes are set or changed, we need to set the
// corresponding value on the internal <input> element. This life cycle
// method, together with the static observedAttributes property below,
// takes care of that.
attributeChangedCallback(name, oldValue, newValue) {
if (name === "disabled") {
this.input.disabled = newValue !== null;
} else if (name === "placeholder") {
this.input.placeholder = newValue;
} else if (name === "size") {
this.input.size = newValue;
} else if (name === "value") {
this.input.value = newValue;
}
}
// Finally, we define property getters and setters for properties that
// correspond to the HTML attributes we support. The getters simply return
// the value (or the presence) of the attribute. And the setters just set
// the value (or the presence) of the attribute. When a setter method
// changes an attribute, the browser will automatically invoke the
// attributeChangedCallback above.
get placeholder() { return this.getAttribute("placeholder"); }
get size() { return this.getAttribute("size"); }
get value() { return this.getAttribute("value"); }
get disabled() { return this.hasAttribute("disabled"); }
get hidden() { return this.hasAttribute("hidden"); }
set placeholder(value) { this.setAttribute("placeholder", value); }
set size(value) { this.setAttribute("size", value); }
set value(text) { this.setAttribute("value", text); }
set disabled(value) {
if (value) this.setAttribute("disabled", "");
else this.removeAttribute("disabled");
}
set hidden(value) {
if (value) this.setAttribute("hidden", "");
else this.removeAttribute("hidden");
}
}
// This static field is required for the attributeChangedCallback method.
// Only attributes named in this array will trigger calls to that method.
SearchBox.observedAttributes = ["disabled", "placeholder", "size", "value"];
// Create a <template> element to hold the stylesheet and the tree of
// elements that we'll use for each instance of the SearchBox element.
SearchBox.template = document.createElement("template");
// We initialize the template by parsing this string of HTML. Note, however,
// that when we instantiate a SearchBox, we are able to just clone the nodes
// in the template and do have to parse the HTML again.
SearchBox.template.innerHTML = `
<style>
/*
* The :host selector refers to the <search-box> element in the light
* DOM. These styles are defaults and can be overridden by the user of the
* <search-box> with styles in the light DOM.
*/
:host {
display: inline-block; /* The default is inline display */
border: solid black 1px; /* A rounded border around the <input> and <slots> */
border-radius: 5px;
padding: 4px 6px; /* And some space inside the border */
}
:host([hidden]) { /* Note the parentheses: when host has hidden... */
display:none; /* ...attribute set don't display it */
}
:host([disabled]) { /* When host has the disabled attribute... */
opacity: 0.5; /* ...gray it out */
}
:host([focused]) { /* When host has the focused attribute... */
box-shadow: 0 0 2px 2px #6AE; /* display this fake focus ring. */
}
/* The rest of the stylesheet only applies to elements in the Shadow DOM. */
input {
border-width: 0; /* Hide the border of the internal input field. */
outline: none; /* Hide the focus ring, too. */
font: inherit; /* <input> elements don't inherit font by default */
background: inherit; /* Same for background color. */
}
slot {
cursor: default; /* An arrow pointer cursor over the buttons */
user-select: none; /* Don't let the user select the emoji text */
}
</style>
<div>
<slot name="left">\u{1f50d}</slot> <!-- U+1F50D is a magnifying glass -->
<input type="text" id="input" /> <!-- The actual input element -->
<slot name="right">\u{2573}</slot> <!-- U+2573 is an X -->
</div>
`;
// Finally, we call customElement.define() to register the SearchBox element
// as the implementation of the <search-box> tag. Custom elements are required
// to have a tag name that contains a hyphen.
customElements.define("search-box", SearchBox);