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:
@ -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>
|
||||
);
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { h, Component } from 'preact';
|
||||
import Modal from './Modal.jsx';
|
||||
import { A } from './common';
|
||||
|
||||
export default class Footer extends Component {
|
||||
|
164
webmaker/src/components/LibraryAutoSuggest.jsx
Normal file
164
webmaker/src/components/LibraryAutoSuggest.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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"
|
||||
|
@ -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 */{'}'}
|
||||
|
@ -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,
|
||||
|
Reference in New Issue
Block a user