mirror of
https://github.com/chinchang/web-maker.git
synced 2025-07-09 16:06:21 +02:00
port add library auto suggestion
This commit is contained in:
@ -1,6 +1,7 @@
|
|||||||
import { h, Component } from 'preact';
|
import { h, Component } from 'preact';
|
||||||
import { jsLibs, cssLibs } from '../libraryList';
|
import { jsLibs, cssLibs } from '../libraryList';
|
||||||
import { trackEvent } from '../analytics';
|
import { trackEvent } from '../analytics';
|
||||||
|
import { LibraryAutoSuggest } from './LibraryAutoSuggest';
|
||||||
|
|
||||||
export default class AddLibrary extends Component {
|
export default class AddLibrary extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@ -20,6 +21,10 @@ export default class AddLibrary extends Component {
|
|||||||
this.setState({
|
this.setState({
|
||||||
js: `${this.state.js}\n${target.value}`
|
js: `${this.state.js}\n${target.value}`
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
css: `${this.state.css}\n${target.value}`
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
trackEvent('ui', 'addLibrarySelect', target.selectedOptions[0].label);
|
trackEvent('ui', 'addLibrarySelect', target.selectedOptions[0].label);
|
||||||
@ -27,17 +32,51 @@ export default class AddLibrary extends Component {
|
|||||||
// Reset the select to the default value
|
// Reset the select to the default value
|
||||||
target.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() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1>Add Library</h1>
|
<h1>Add Library</h1>
|
||||||
|
|
||||||
<input
|
<div class="flex">
|
||||||
type="text"
|
<svg style="width: 30px; height: 30px;fill:#999">
|
||||||
id="externalLibrarySearchInput"
|
<use xlinkHref="#search" />
|
||||||
class="full-width"
|
</svg>
|
||||||
placeholder="Type here to search libraries"
|
<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">
|
<div class="tar opacity--70">
|
||||||
<small>Powered by cdnjs</small>
|
<small>Powered by cdnjs</small>
|
||||||
</div>
|
</div>
|
||||||
@ -66,7 +105,9 @@ export default class AddLibrary extends Component {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</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">
|
<p style="font-size: 0.8em;" class="show-when-extension opacity--70">
|
||||||
Note: You can load external scripts from following domains: localhost,
|
Note: You can load external scripts from following domains: localhost,
|
||||||
https://ajax.googleapis.com, https://code.jquery.com,
|
https://ajax.googleapis.com, https://code.jquery.com,
|
||||||
@ -76,23 +117,27 @@ export default class AddLibrary extends Component {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<textarea
|
<textarea
|
||||||
onBlur={this.props.onChange}
|
onBlur={this.textareaBlurHandler.bind(this)}
|
||||||
|
data-lang="js"
|
||||||
class="full-width"
|
class="full-width"
|
||||||
id=""
|
id="externalJsTextarea"
|
||||||
cols="30"
|
cols="30"
|
||||||
rows="5"
|
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}
|
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
|
<textarea
|
||||||
onBlur={this.props.onChange}
|
onBlur={this.textareaBlurHandler.bind(this)}
|
||||||
|
data-lang="css"
|
||||||
class="full-width"
|
class="full-width"
|
||||||
id=""
|
id="externalCssTextarea"
|
||||||
cols="30"
|
cols="30"
|
||||||
rows="5"
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { h, Component } from 'preact';
|
import { h, Component } from 'preact';
|
||||||
import Modal from './Modal.jsx';
|
|
||||||
import { A } from './common';
|
import { A } from './common';
|
||||||
|
|
||||||
export default class Footer extends Component {
|
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();
|
this.props.closeHandler();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
componentDidUpdate() {
|
componentDidUpdate(prevProps) {
|
||||||
document.body.classList[this.props.show ? 'add' : 'remove'](
|
document.body.classList[this.props.show ? 'add' : 'remove'](
|
||||||
'overlay-visible'
|
'overlay-visible'
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.props.show) {
|
if (this.props.show && !prevProps.show) {
|
||||||
this.overlayEl.querySelector('.js-modal__close-btn').focus();
|
this.overlayEl.querySelector('.js-modal__close-btn').focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -37,6 +37,7 @@ export default class Modal extends Component {
|
|||||||
>
|
>
|
||||||
<div class="modal__content">
|
<div class="modal__content">
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={this.props.closeHandler}
|
onClick={this.props.closeHandler}
|
||||||
aria-label="Close modal"
|
aria-label="Close modal"
|
||||||
title="Close"
|
title="Close"
|
||||||
|
@ -1190,6 +1190,9 @@ export default class App extends Component {
|
|||||||
<rect x={69} y={0} width={32} height={100} />
|
<rect x={69} y={0} width={32} height={100} />
|
||||||
</g>
|
</g>
|
||||||
</symbol>
|
</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">
|
<symbol id="loader-icon" viewBox="0 0 44 44">
|
||||||
{'{'}/* By Sam Herbert (@sherb), for everyone. More @
|
{'{'}/* By Sam Herbert (@sherb), for everyone. More @
|
||||||
http://goo.gl/7AJzbL */{'}'}
|
http://goo.gl/7AJzbL */{'}'}
|
||||||
|
@ -108,6 +108,14 @@ p {
|
|||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mb-0 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mt-0 {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 500px) {
|
@media screen and (max-width: 500px) {
|
||||||
.block--mobile {
|
.block--mobile {
|
||||||
display: block;
|
display: block;
|
||||||
@ -1445,7 +1453,6 @@ body:not(.is-app) .show-when-app {
|
|||||||
.help-text {
|
.help-text {
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
color: #616465;
|
color: #616465;
|
||||||
padding-left: 1em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.social-login-btn:after,
|
.social-login-btn:after,
|
||||||
|
Reference in New Issue
Block a user