1
0
mirror of https://github.com/chinchang/web-maker.git synced 2025-07-10 00:16:18 +02:00

port add library auto suggestion

This commit is contained in:
Kushagra Gour
2018-06-15 17:15:57 +05:30
parent 29687a748a
commit d281bb7e53
6 changed files with 237 additions and 18 deletions

View File

@ -1,6 +1,7 @@
import { h, Component } from 'preact';
import { jsLibs, cssLibs } from '../libraryList';
import { trackEvent } from '../analytics';
import { LibraryAutoSuggest } from './LibraryAutoSuggest';
export default class AddLibrary extends Component {
constructor(props) {
@ -20,6 +21,10 @@ export default class AddLibrary extends Component {
this.setState({
js: `${this.state.js}\n${target.value}`
});
} else {
this.setState({
css: `${this.state.css}\n${target.value}`
});
}
trackEvent('ui', 'addLibrarySelect', target.selectedOptions[0].label);
@ -27,17 +32,51 @@ export default class AddLibrary extends Component {
// Reset the select to the default value
target.value = '';
}
textareaBlurHandler(e, textarea) {
const target = e ? e.target : textarea;
const type = target.dataset.lang;
if (type === 'js') {
this.setState({
js: target.value || ''
});
} else {
this.setState({
css: target.value || ''
});
}
// trackEvent('ui', 'addLibrarySelect', target.selectedOptions[0].label);
this.props.onChange({ js: this.state.js, css: this.state.css });
}
suggestionSelectHandler(value) {
const textarea = value.match(/\.js$/)
? window.externalJsTextarea
: window.externalCssTextarea;
textarea.value = `${textarea.value}\n${value}`;
window.externalLibrarySearchInput.value = '';
this.textareaBlurHandler(null, textarea);
}
render() {
return (
<div>
<h1>Add Library</h1>
<input
type="text"
id="externalLibrarySearchInput"
class="full-width"
placeholder="Type here to search libraries"
/>
<div class="flex">
<svg style="width: 30px; height: 30px;fill:#999">
<use xlinkHref="#search" />
</svg>
<LibraryAutoSuggest
fullWidth
onSelect={this.suggestionSelectHandler.bind(this)}
>
<input
type="text"
id="externalLibrarySearchInput"
class="full-width"
placeholder="Type here to search libraries"
/>
</LibraryAutoSuggest>
</div>
<div class="tar opacity--70">
<small>Powered by cdnjs</small>
</div>
@ -66,7 +105,9 @@ export default class AddLibrary extends Component {
</select>
</div>
<h3>JavaScript</h3>
<h3 class="mb-0">JS</h3>
<p class="mt-0 help-text">Put each library in new line</p>
<p style="font-size: 0.8em;" class="show-when-extension opacity--70">
Note: You can load external scripts from following domains: localhost,
https://ajax.googleapis.com, https://code.jquery.com,
@ -76,23 +117,27 @@ export default class AddLibrary extends Component {
</p>
<textarea
onBlur={this.props.onChange}
onBlur={this.textareaBlurHandler.bind(this)}
data-lang="js"
class="full-width"
id=""
id="externalJsTextarea"
cols="30"
rows="5"
placeholder="Start typing name of a library. Put each library in new line"
placeholder="Put each library in new line"
value={this.state.js}
/>
<h3>CSS</h3>
<h3 class="mb-0">CSS</h3>
<p class="mt-0 help-text">Put each library in new line</p>
<textarea
onBlur={this.props.onChange}
onBlur={this.textareaBlurHandler.bind(this)}
data-lang="css"
class="full-width"
id=""
id="externalCssTextarea"
cols="30"
rows="5"
placeholder="Start typing name of a library. Put each library in new line"
placeholder="Put each library in new line"
value={this.state.css}
/>
</div>
);

View File

@ -1,5 +1,4 @@
import { h, Component } from 'preact';
import Modal from './Modal.jsx';
import { A } from './common';
export default class Footer extends Component {

View File

@ -0,0 +1,164 @@
import { h, Component } from 'preact';
import { trackEvent } from '../analytics';
export class LibraryAutoSuggest extends Component {
componentDidMount() {
this.t = this.wrap.querySelector('input,textarea');
this.filter = this.props.filter;
this.selectedCallback = this.props.onSelect;
// after list is insrted into the DOM, we put it in the body
// fixed at same position
setTimeout(() => {
requestIdleCallback(() => {
document.body.appendChild(this.list);
this.list.style.position = 'fixed';
});
}, 100);
this.t.addEventListener('input', e => this.onInput(e));
this.t.addEventListener('keydown', e => this.onKeyDown(e));
this.t.addEventListener('blur', e => this.closeSuggestions(e));
this.list.addEventListener('mousedown', e => this.onListMouseDown(e));
}
componentWillUnmount() {
this.t.removeEventListener('input', e => this.onInput(e));
this.t.removeEventListener('keydown', e => this.onKeyDown(e));
this.t.removeEventListener('blur', e => this.closeSuggestions(e));
this.list.removeEventListener('mousedown', e => this.onListMouseDown(e));
}
get currentLineNumber() {
return this.t.value.substr(0, this.t.selectionStart).split('\n').length;
}
get currentLine() {
var line = this.currentLineNumber;
return this.t.value.split('\n')[line - 1];
}
closeSuggestions() {
this.list.classList.remove('is-open');
this.isShowingSuggestions = false;
}
getList(input) {
var url = 'https://api.cdnjs.com/libraries?search=';
return fetch(url + input).then(response => {
return response.json().then(json => json.results);
});
}
replaceCurrentLine(val) {
var lines = this.t.value.split('\n');
lines.splice(this.currentLineNumber - 1, 1, val);
this.t.value = lines.join('\n');
}
onInput() {
var currentLine = this.currentLine;
if (currentLine) {
if (currentLine.indexOf('/') !== -1 || currentLine.match(/https*:\/\//)) {
return;
}
clearTimeout(this.timeout);
this.timeout = setTimeout(() => {
this.loader.style.display = 'block';
this.getList(currentLine).then(arr => {
this.loader.style.display = 'none';
if (!arr.length) {
this.closeSuggestions();
return;
}
this.list.innerHTML = '';
if (this.filter) {
/* eslint-disable no-param-reassign */
arr = arr.filter(this.filter);
}
for (var i = 0; i < Math.min(arr.length, 10); i++) {
this.list.innerHTML += `<li data-url="${arr[i].latest}"><a>${
arr[i].name
}</a></li>`;
}
this.isShowingSuggestions = true;
if (!this.textareaBounds) {
this.textareaBounds = this.t.getBoundingClientRect();
this.list.style.top = this.textareaBounds.bottom + 'px';
this.list.style.left = this.textareaBounds.left + 'px';
this.list.style.width = this.textareaBounds.width + 'px';
}
this.list.classList.add('is-open');
});
}, 500);
}
}
onKeyDown(event) {
var selectedItemElement;
if (!this.isShowingSuggestions) {
return false;
}
if (event.keyCode === 27) {
this.closeSuggestions();
event.stopPropagation();
}
if (event.keyCode === 40 && this.isShowingSuggestions) {
selectedItemElement = this.list.querySelector('.selected');
if (selectedItemElement) {
selectedItemElement.classList.remove('selected');
selectedItemElement.nextElementSibling.classList.add('selected');
} else {
this.list.querySelector('li:first-child').classList.add('selected');
}
this.list.querySelector('.selected').scrollIntoView(false);
event.preventDefault();
} else if (event.keyCode === 38 && this.isShowingSuggestions) {
selectedItemElement = this.list.querySelector('.selected');
if (selectedItemElement) {
selectedItemElement.classList.remove('selected');
selectedItemElement.previousElementSibling.classList.add('selected');
} else {
this.list.querySelector('li:first-child').classList.add('selected');
}
this.list.querySelector('.selected').scrollIntoView(false);
event.preventDefault();
} else if (event.keyCode === 13 && this.isShowingSuggestions) {
selectedItemElement = this.list.querySelector('.selected');
this.selectSuggestion(selectedItemElement.dataset.url);
this.closeSuggestions();
}
}
onListMouseDown(event) {
var target = event.target;
if (target.parentElement.dataset.url) {
this.selectSuggestion(target.parentElement.dataset.url);
}
}
selectSuggestion(value) {
// Return back the focus which is getting lost for some reason
this.t.focus();
trackEvent('ui', 'autoSuggestionLibSelected', value);
if (this.selectedCallback) {
this.selectedCallback.call(null, value);
} else {
this.replaceCurrentLine(value);
}
this.closeSuggestions();
}
render() {
return (
<div
class={`btn-group ${this.props.fullWidth ? 'flex-grow' : ''}`}
ref={el => (this.wrap = el)}
>
{this.props.children}
<ul
ref={el => (this.list = el)}
class="dropdown__menu autocomplete-dropdown"
/>
<div
ref={el => (this.loader = el)}
class="loader autocomplete__loader"
style="display:none"
/>
</div>
);
}
}

View File

@ -17,12 +17,12 @@ export default class Modal extends Component {
this.props.closeHandler();
}
}
componentDidUpdate() {
componentDidUpdate(prevProps) {
document.body.classList[this.props.show ? 'add' : 'remove'](
'overlay-visible'
);
if (this.props.show) {
if (this.props.show && !prevProps.show) {
this.overlayEl.querySelector('.js-modal__close-btn').focus();
}
}
@ -37,6 +37,7 @@ export default class Modal extends Component {
>
<div class="modal__content">
<button
type="button"
onClick={this.props.closeHandler}
aria-label="Close modal"
title="Close"

View File

@ -1190,6 +1190,9 @@ export default class App extends Component {
<rect x={69} y={0} width={32} height={100} />
</g>
</symbol>
<symbol id="search" viewBox="0 0 24 24">
<path d="M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z" />
</symbol>
<symbol id="loader-icon" viewBox="0 0 44 44">
{'{'}/* By Sam Herbert (@sherb), for everyone. More @
http://goo.gl/7AJzbL */{'}'}

View File

@ -108,6 +108,14 @@ p {
margin-bottom: 2rem;
}
.mb-0 {
margin-bottom: 0;
}
.mt-0 {
margin-top: 0;
}
@media screen and (max-width: 500px) {
.block--mobile {
display: block;
@ -1445,7 +1453,6 @@ body:not(.is-app) .show-when-app {
.help-text {
font-size: 0.9em;
color: #616465;
padding-left: 1em;
}
.social-login-btn:after,