js/controllers/StackExchangeImportViewController.js
import ProgressButtonController from '~/controllers/ProgressButtonController';
import ViewController from '~/controllers/ViewController';
import ErrorManager from '~/helpers/ErrorManager';
import Normalize from '~/models/Normalize';
import Query from '~/models/Query';
import Theme from '~/models/Theme';
import Post from '~/models/Request/Post';
import removeMarkdown from 'remove-markdown';
function styleName(name) {
return `create-post__StackExchangeImport__${name}`;
}
/**
* Imports from Stack Exchange
*/
export default class StackExchangeImporterViewController extends ViewController {
/**
* The instance container. Also takes in a Stack Exchange instance
* @param {HTMLElement} container
* @param {StackExchange} stackExchange Must be auth'd
*/
constructor(container, stackExchange) {
super(container);
/** @type {HTMLElement} */
this.container = container;
/** @type {StackExchange} */
this.stackExchange = stackExchange;
/** @type {Object} */
this.questions = [];
/** @type {?HTMLElement} */
this.questionList = null;
/** @type {Query} */
this.query = null;
/** @type {ProgressButtonController} */
this.importButton = null;
/** @type {Object} */
this.selectedQuestion = null;
this.initialize()
.catch(ErrorManager.unhandled);
}
async initialize() {
this.setLoading();
const posts = await this.stackExchange.getQuestions();
this.questions = posts;
// Generate query
this.query = new Query(
posts,
post => post.title
);
this.setClear();
this.container.appendChild(
<div class={`body ${styleName('instruction')}`}>Select the post you would like to import</div>
);
// Generate search box
const search = (
<input type="text" class="text-input text-input--size-wide text-input--type-search text-input--pad-top" placeholder="Search..." />
);
search.addEventListener('input', () => {
this.setSearchFilter(search.value);
});
this.container.appendChild(search);
this.questionList = <div class={styleName('scrollBox')}></div>
this.showQuestions(posts);
this.container.appendChild(this.questionList);
// Add the upload button
const importButton = (
<div class="button button--color-accent button--size-wide button--align-center">
<img src={ Theme.dark.imageForTheme('import') } />
<span>Import</span>
</div>
);
this.importButton = new ProgressButtonController(importButton);
importButton.addEventListener('click', ::this.import);
this.container.appendChild(importButton);
// We don't want the size of the window to keep changing
this.container.style.width = window.getComputedStyle(this.container).width;
this.container.style.height = window.getComputedStyle(this.container).height;
}
_importing = false;
/**
* Runs the import
*/
async import() {
if (this._importing) return;
this.importing = true;
// Find selected question
if (!this.selectedQuestion) {
alert('You must select a question to import');
this.importing = false;
return;
}
const post = new Post({
title: this.selectedQuestion.title,
body: this.normalize(this.selectedQuestion.body_markdown),
ppcgId: this.selectedQuestion.question_id
});
const redirectUrl = await post.run();
window.location.href = redirectUrl;
this.importing = false;
}
/**
* Normalized the body to support Axtell Markdown
* @param {string} sourceMarkdown
* @return {string}
*/
normalize(sourceMarkdown) {
return sourceMarkdown
.replace(/(\s)\\\$|\\\$(\s)/g,'$1$$$2') // Normalize LaTeX delimiters
.replace(/(^|\n)(#+)([a-z])/gi, '$1$2 $3') // Normalize headers (space)
.replace(/(.)\n(#+)/g, '$1\n\n$2') // Add extra line so headers are on own line
.replace(/\[tag:([a-z-]+)\]/gi, '[$1](https://codegolf.stackexchange.com/questions/tagged/$1)') // Convert tags to links
}
/**
* Sets if importing or not
* @param {boolean} value if importing
*/
set importing(value) {
this._importing = value;
this.importButton.setLoadingState(value);
}
// Search logic
_lastFilter = null;
/**
* Sets the filter on the search to a value
* @param {string} value Empty value means all
*/
setSearchFilter(value) {
if (this._lastFilter) clearTimeout(this._lastFilter);
this._lastFilter = setTimeout(() => {
this._lastFilter = null;
if (value) {
this.showQuestions(this.query.find(value));
} else {
this.showQuestions(this.questions);
}
}, 50);
}
/**
* Clears all questions
*/
clearQuestions() {
while(this.questionList.firstChild) {
this.questionList.removeChild(this.questionList.firstChild);
}
}
/**
* Shows the questions
* @param {Object[]} posts - The questinos to show
*/
showQuestions(posts) {
this.clearQuestions();
const fragment = document.createDocumentFragment();
posts.map(post => {
fragment.appendChild(this.createOptionFor(post));
});
this.questionList.appendChild(fragment);
}
/**
* Creates an <option> from an SE API Question object
* @param {Object} question from SE API
*/
createOptionFor(question) {
const itemId = styleName(`question_${question.question_id}`);
const itemLabel = `${itemId}__label`;
const title = question.title;
const bodyPreview = removeMarkdown(question.body_markdown).substring(0, 80);
const input = (
<input
type="radio" name={styleName('radio')} class={styleName('radio')} id={itemId}
data-qid={question.question_id} aria-describedby={itemLabel} />
);
// Handled updates correctly
if (question.question_id === this.selectedQuestion?.question_id) {
input.checked = true;
}
input.addEventListener('change', () => {
if (input.checked) {
this.selectedQuestion = question;
}
});
return (
<div class={styleName('question')}>
{ input }
<label for={itemId} id={itemLabel} class={styleName('question__overview')}>
<h4 class={styleName('question__title')}>{ title }</h4>
<span class={styleName('question__desc')}>{ bodyPreview }...</span>
</label>
<a href={ question.link } class={styleName('link')} target="_blank" rel="external nofollow">
<img class={styleName('link__icon')} src={ Theme.current.imageForTheme('external-link') } />
</a>
</div>
);
}
/**
* Clears all elements
*/
setClear() {
while(this.container.firstChild) {
this.container.removeChild(this.container.firstChild);
}
}
/**
* Sets this to the loading state
*/
setLoading() {
this.setClear();
this.container.appendChild(
<img class={styleName('loading')} src={Theme.current.imageForTheme('loading')} />
);
}
}