Mailspring/app/internal_packages/composer-templates/lib/preferences-templates.jsx

395 lines
11 KiB
JavaScript

import _ from 'underscore';
import { Contenteditable, RetinaImg } from 'mailspring-component-kit';
import { React } from 'mailspring-exports';
import TemplateStore from './template-store';
import TemplateEditor from './template-editor';
class PreferencesTemplates extends React.Component {
static displayName = 'PreferencesTemplates';
constructor() {
super();
this._templateSaveQueue = {};
const { templates, selectedTemplate, selectedTemplateName } = this._getStateFromStores();
this.state = {
editAsHTML: false,
editState: templates.length === 0 ? 'new' : null,
templates: templates,
selectedTemplate: selectedTemplate,
selectedTemplateName: selectedTemplateName,
contents: null,
};
}
componentDidMount() {
this.unsub = TemplateStore.listen(this._onChange);
}
componentWillUnmount() {
this.unsub();
if (this.state.selectedTemplate) {
this._saveTemplateNow(this.state.selectedTemplate.name, this.state.contents);
}
}
// SAVING AND LOADING TEMPLATES
_loadTemplateContents = template => {
if (template) {
TemplateStore.getTemplateContents(template.id, contents => {
this.setState({ contents: contents });
});
}
};
_saveTemplateNow(name, contents, callback) {
TemplateStore.saveTemplate(name, contents, callback);
}
_saveTemplateSoon(name, contents) {
this._templateSaveQueue[name] = contents;
this._saveTemplatesFromCache();
}
__saveTemplatesFromCache() {
for (const name of Object.keys(this._templateSaveQueue)) {
this._saveTemplateNow(name, this._templateSaveQueue[name]);
}
this._templateSaveQueue = {};
}
_saveTemplatesFromCache = _.debounce(
PreferencesTemplates.prototype.__saveTemplatesFromCache,
500
);
// OVERALL STATE HANDLING
_onChange = () => {
this.setState(this._getStateFromStores());
};
_getStateFromStores() {
const templates = TemplateStore.items();
let selectedTemplate = this.state ? this.state.selectedTemplate : null;
if (selectedTemplate && !_.pluck(templates, 'id').includes(selectedTemplate.id)) {
selectedTemplate = null;
} else if (!selectedTemplate) {
selectedTemplate = templates.length > 0 ? templates[0] : null;
}
this._loadTemplateContents(selectedTemplate);
let selectedTemplateName = null;
if (selectedTemplate) {
selectedTemplateName = this.state ? this.state.selectedTemplateName : selectedTemplate.name;
}
return { templates, selectedTemplate, selectedTemplateName };
}
// TEMPLATE CONTENT EDITING
_onEditTemplate = event => {
const html = event.target.value;
this.setState({ contents: html });
if (this.state.selectedTemplate) {
this._saveTemplateSoon(this.state.selectedTemplate.name, html);
}
};
_onSelectTemplate = event => {
if (this.state.selectedTemplate) {
this._saveTemplateNow(this.state.selectedTemplate.name, this.state.contents);
}
const selectedId = event.target.value;
const selectedTemplate = this.state.templates.find(template => template.id === selectedId);
this.setState({
selectedTemplate: selectedTemplate,
selectedTemplateName: selectedTemplate ? selectedTemplate.name : null,
contents: null,
});
this._loadTemplateContents(selectedTemplate);
};
_renderTemplatePicker() {
const options = this.state.templates.map(template => {
return (
<option value={template.id} key={template.id}>
{template.name}
</option>
);
});
return (
<select
value={this.state.selectedTemplate ? this.state.selectedTemplate.id : null}
onChange={this._onSelectTemplate}
>
{options}
</select>
);
}
_renderEditableTemplate() {
return (
<Contenteditable
value={this.state.contents || ''}
onChange={this._onEditTemplate}
extensions={[TemplateEditor]}
spellcheck={false}
/>
);
}
_renderHTMLTemplate() {
return <textarea value={this.state.contents || ''} onChange={this._onEditTemplate} />;
}
_renderModeToggle() {
if (this.state.editAsHTML) {
return (
<a
onClick={() => {
this.setState({ editAsHTML: false });
}}
>
Edit live preview
</a>
);
}
return (
<a
onClick={() => {
this.setState({ editAsHTML: true });
}}
>
Edit raw HTML
</a>
);
}
_onEnter(action) {
return event => {
if (event.key === 'Enter') {
action();
}
};
}
// TEMPLATE NAME EDITING
_renderEditName() {
return (
<div className="section-title">
Template Name:{' '}
<input
type="text"
className="template-name-input"
value={this.state.selectedTemplateName}
onChange={this._onEditName}
onKeyDown={this._onEnter(this._saveName)}
/>
<button className="btn template-name-btn" onClick={this._saveName}>
Save Name
</button>
<button className="btn template-name-btn" onClick={this._cancelEditName}>
Cancel
</button>
</div>
);
}
_renderName() {
const rawText = this.state.editAsHTML ? 'Raw HTML ' : '';
return (
<div className="section-title">
{rawText}Template: {this._renderTemplatePicker()}
<button
className="btn template-name-btn"
title="New template"
onClick={this._startNewTemplate}
>
New
</button>
<button
className="btn template-name-btn"
onClick={() => {
this.setState({ editState: 'name' });
}}
>
Rename
</button>
</div>
);
}
_onEditName = event => {
this.setState({ selectedTemplateName: event.target.value });
};
_cancelEditName = () => {
this.setState({
selectedTemplateName: this.state.selectedTemplate ? this.state.selectedTemplate.name : null,
editState: null,
});
};
_saveName = () => {
if (
this.state.selectedTemplate &&
this.state.selectedTemplate.name !== this.state.selectedTemplateName
) {
TemplateStore.renameTemplate(
this.state.selectedTemplate.name,
this.state.selectedTemplateName,
renamedTemplate => {
this.setState({
selectedTemplate: renamedTemplate,
editState: null,
});
}
);
} else {
this.setState({
editState: null,
});
}
};
// DELETE AND NEW
_deleteTemplate = () => {
const numTemplates = this.state.templates.length;
if (this.state.selectedTemplate) {
TemplateStore.deleteTemplate(this.state.selectedTemplate.name);
}
if (numTemplates === 1) {
this.setState({
editState: 'new',
selectedTemplate: null,
selectedTemplateName: '',
contents: '',
});
}
};
_startNewTemplate = () => {
this.setState({
editState: 'new',
selectedTemplate: null,
selectedTemplateName: '',
contents: '',
});
};
_saveNewTemplate = () => {
this.setState({ contents: '' });
TemplateStore.saveNewTemplate(this.state.selectedTemplateName, '', template => {
this.setState({
selectedTemplate: template,
editState: null,
});
});
};
_cancelNewTemplate = () => {
const template = this.state.templates.length > 0 ? this.state.templates[0] : null;
this.setState({
selectedTemplate: template,
selectedTemplateName: template ? template.name : null,
editState: null,
});
this._loadTemplateContents(template);
};
_renderCreateNew() {
const cancel = (
<button className="btn template-name-btn" onClick={this._cancelNewTemplate}>
Cancel
</button>
);
return (
<div className="section-title">
Template Name:{' '}
<input
type="text"
className="template-name-input"
value={this.state.selectedTemplateName}
onChange={this._onEditName}
onKeyDown={this._onEnter(this._saveNewTemplate)}
/>
<button className="btn btn-emphasis template-name-btn" onClick={this._saveNewTemplate}>
Save
</button>
{this.state.templates.length ? cancel : null}
</div>
);
}
// MAIN RENDER
render() {
const deleteBtn = (
<button className="btn" title="Delete template" onClick={this._deleteTemplate}>
<RetinaImg name="icon-composer-trash.png" mode={RetinaImg.Mode.ContentIsMask} />
</button>
);
const editor = (
<div>
<div className="template-wrap">
{this.state.editAsHTML ? this._renderHTMLTemplate() : this._renderEditableTemplate()}
</div>
<div style={{ marginTop: '5px' }}>
<span className="editor-note">
{_.size(this._templateSaveQueue) === 0 ? 'Changes saved.' : ''}
&nbsp;
</span>
<span style={{ float: 'right' }}>{this.state.editState === null ? deleteBtn : ''}</span>
</div>
<div className="toggle-mode" style={{ marginTop: '1em' }}>
{this._renderModeToggle()}
</div>
</div>
);
let editContainer = this._renderName();
if (this.state.editState === 'name') {
editContainer = this._renderEditName();
} else if (this.state.editState === 'new') {
editContainer = this._renderCreateNew();
}
const noTemplatesMessage = (
<div className="template-status-bar no-templates-message">
{`You don't have any templates! Enter a template name and press save to create one.`}
</div>
);
return (
<div className="container-templates">
<section style={this.state.editState === 'new' ? { marginBottom: 50 } : null}>
{editContainer}
{this.state.editState !== 'new' ? editor : null}
{this.state.templates.length === 0 ? noTemplatesMessage : null}
</section>
<section className="templates-instructions">
<p>
{`To create a variable, type a set of double curly
brackets wrapping the variable's name, like this`}:{' '}
<strong>
{'{{'}variable_name{'}}'}
</strong>. The highlighting in the variable regions will be removed before the message
is sent.
</p>
<p>
Reply templates are saved as HTML files in the{' '}
<strong>{`${AppEnv.getConfigDirPath()}/templates`}</strong> directory on your computer.
In raw HTML, variables are defined as HTML &lt;code&gt; tags with class &quot;var
empty&quot;.
</p>
</section>
</div>
);
}
}
export default PreferencesTemplates;