amis2/examples/components/Play.jsx

622 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React from 'react';
import {toast, render, makeTranslator} from 'amis';
import {normalizeLink} from 'amis-core';
import {isMobile} from 'amis-core';
import {attachmentAdpator} from 'amis-core';
import {alert, confirm} from 'amis-ui';
import axios from 'axios';
import JSON5 from 'json5';
import {Editor as CodeEditor} from 'amis-ui';
import copy from 'copy-to-clipboard';
import {matchPath} from 'react-router-dom';
import {Drawer} from 'amis-ui';
const DEFAULT_CONTENT = `{
"$schema": "/schemas/page.json#",
"type": "page",
"title": "Title",
"body": "Body",
"aside": "Aside",
"toolbar": "Toolbar"
}`;
const scopes = {
'none': ``,
'body': `{
"type": "page",
"body": SCHEMA_PLACEHOLDER
}`,
'form': `{
"type": "page",
"body": {
"title": "",
"type": "form",
"autoFocus": false,
"api": "/api/mock/saveForm?waitSeconds=1",
"mode": "horizontal",
"body": SCHEMA_PLACEHOLDER,
"submitText": null,
"actions": []
}
}`,
'form2': `{
"type": "page",
"body": {
"title": "",
"type": "form",
"autoFocus": false,
"api": "/api/mock/saveForm?waitSeconds=1",
"mode": "horizontal",
"body": SCHEMA_PLACEHOLDER,
"submitText": null,
"actions": []
}
}`,
'form-item': `{
"type": "page",
"body": {
"title": "",
"type": "form",
"mode": "horizontal",
"autoFocus": false,
"body": [
SCHEMA_PLACEHOLDER
],
"submitText": null,
"actions": []
}
}`,
'form-item2': `{
"type": "page",
"body": {
"title": "",
"type": "form",
"mode": "horizontal",
"autoFocus": false,
"body": [
SCHEMA_PLACEHOLDER
],
"submitText": null,
"actions": []
}
}`
};
export default class PlayGround extends React.Component {
state = null;
startX = 0;
oldContents = '';
frameTemplate;
iframeRef;
static defaultProps = {
vertical: false
};
constructor(props) {
super(props);
this.iframeRef = React.createRef();
const {history} = props;
const schema = this.buildSchema(props.code || DEFAULT_CONTENT, props);
this.state = {
asideWidth: props.asideWidth || Math.max(300, window.innerWidth * 0.3),
schema: schema,
schemaCode: JSON.stringify(schema, null, 2),
isOpened: false
};
this.handleMouseDown = this.handleMouseDown.bind(this);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.handleMouseUp = this.handleMouseUp.bind(this);
this.removeWindowEvents = this.removeWindowEvents.bind(this);
this.handleChange = this.handleChange.bind(this);
this.toggleDrawer = this.toggleDrawer.bind(this);
this.close = this.close.bind(this);
this.schemaProps = {};
const __ = makeTranslator(props.locale);
this.env = {
session: 'doc',
updateLocation: (location, replace) => {
history[replace ? 'replace' : 'push'](normalizeLink(location));
},
isCurrentUrl: to => {
if (!to) {
return false;
}
const link = normalizeLink(to);
return !!matchPath(history.location.pathname, {
path: link,
exact: true
});
},
jumpTo: (to, action) => {
to = normalizeLink(to);
if (action && action.actionType === 'url') {
action.blank === true ? window.open(to) : (window.location.href = to);
return;
}
if (action && to && action.target) {
window.open(to, action.target);
return;
}
if (/^https?:\/\//.test(to)) {
window.location.replace(to);
} else {
history.push(to);
}
},
fetcher: async api => {
let {url, method, data, responseType, config, headers} = api;
config = config || {};
// 如果在 gh-pages 里面
if (
/^\/amis/.test(window.location.pathname) &&
typeof url === 'string' &&
url.startsWith('/examples/static/')
) {
url = url.replace('/examples/static/', '/amis/static/');
}
config.url = url;
responseType && (config.responseType = responseType);
if (config.cancelExecutor) {
config.cancelToken = new axios.CancelToken(config.cancelExecutor);
}
config.headers = headers || {};
config.method = method;
config.data = data;
if (method === 'get' && data) {
config.params = data;
} else if (data && data instanceof FormData) {
// config.headers['Content-Type'] = 'multipart/form-data';
} else if (
data &&
typeof data !== 'string' &&
!(data instanceof Blob) &&
!(data instanceof ArrayBuffer)
) {
data = JSON.stringify(data);
config.headers['Content-Type'] = 'application/json';
}
// 支持返回各种报错信息
config.validateStatus = function () {
return true;
};
let response = await axios(config);
response = await attachmentAdpator(response, __, api);
if (response.status >= 400) {
if (response.data) {
// 主要用于 raw: 模式下,后端自己校验登录,
if (
response.status === 401 &&
response.data.location &&
response.data.location.startsWith('http')
) {
location.href = response.data.location.replace(
'{{redirect}}',
encodeURIComponent(location.href)
);
return new Promise(() => {});
} else if (response.data.msg) {
throw new Error(response.data.msg);
} else {
throw new Error(
__('System.requestError') +
JSON.stringify(response.data, null, 2)
);
}
} else {
throw new Error(
`${__('System.requestErrorStatus')} ${response.status}`
);
}
}
return response;
},
isCancel: value => axios.isCancel(value),
notify: (type, msg, conf) =>
toast[type]
? toast[type](msg, conf)
: console.warn('[Notify]', type, msg),
alert,
confirm,
copy: (content, options) => {
copy(content, options);
toast.success(__('System.copy'));
},
tracker(eventTrack) {
console.debug('eventTrack', eventTrack);
},
replaceText: {
AMIS_HOST: 'https://baidu.gitee.io/amis'
},
loadTinymcePlugin: async tinymce => {
// 参考https://www.tiny.cloud/docs/advanced/creating-a-plugin/
/*
Note: We have included the plugin in the same JavaScript file as the TinyMCE
instance for display purposes only. Tiny recommends not maintaining the plugin
with the TinyMCE instance and using the `external_plugins` option.
*/
tinymce.PluginManager.add('example', function (editor, url) {
var openDialog = function () {
return editor.windowManager.open({
title: 'Example plugin',
body: {
type: 'panel',
items: [
{
type: 'input',
name: 'title',
label: 'Title'
}
]
},
buttons: [
{
type: 'cancel',
text: 'Close'
},
{
type: 'submit',
text: 'Save',
primary: true
}
],
onSubmit: function (api) {
var data = api.getData();
/* Insert content when the window form is submitted */
editor.insertContent('Title: ' + data.title);
api.close();
}
});
};
/* Add a button that opens a window */
editor.ui.registry.addButton('example', {
text: 'My button',
onAction: function () {
/* Open window */
openDialog();
}
});
/* Adds a menu item, which can then be included in any menu via the menu/menubar configuration */
editor.ui.registry.addMenuItem('example', {
text: 'Example plugin',
onAction: function () {
/* Open window */
openDialog();
}
});
/* Return the metadata for the help plugin */
return {
getMetadata: function () {
return {
name: 'Example plugin',
url: 'http://exampleplugindocsurl.com'
};
}
};
});
}
};
this.watchIframeReady = this.watchIframeReady.bind(this);
window.addEventListener('message', this.watchIframeReady, false);
}
watchIframeReady(event) {
// iframe 里面的 amis 初始化了就可以发数据
if (event.data && event.data === 'amisReady') {
this.updateIframe();
}
}
updateIframe() {
if (this.iframeRef && this.iframeRef.current) {
this.iframeRef.current.contentWindow.postMessage(
{
schema: this.state.schema,
props: {theme: this.props.theme, locale: this.props.locale}
},
'*'
);
}
}
componentDidUpdate(preProps) {
const props = this.props;
if (preProps.code !== props.code) {
const schema = this.buildSchema(props.code || DEFAULT_CONTENT, props);
this.setState({
schema: schema,
schemaCode: JSON.stringify(schema, null, 2)
});
}
}
componentDidMount() {
this.props.setAsideFolded && this.props.setAsideFolded(true);
}
componentWillUnmount() {
this.props.setAsideFolded && this.props.setAsideFolded(false);
window.removeEventListener('message', this.watchIframeReady, false);
}
buildSchema(schemaContent, props = this.props) {
const query = props.location.query;
try {
const scope = props.scope;
if (scope && scopes[scope]) {
schemaContent = scopes[scope].replace(
'SCHEMA_PLACEHOLDER',
schemaContent
);
}
schemaContent = schemaContent.replace(/('|")raw:/g, '$1'); // 去掉 raw
const json = JSON5.parse(schemaContent);
return json;
} catch (e) {
console.error(this.formatMessage(e.message, schemaContent));
}
return {};
}
formatMessage(message, input) {
if (/position\s?(\d+)$/.test(message)) {
const lines = input
.substring(0, parseInt(RegExp.$1, 10))
.split(/\n|\r\n|\r/);
message = `Json 语法错误,请检测。出错位置:${lines.length},列:${
lines[lines.length - 1].length
}`;
}
return message;
}
renderPreview() {
const schema = this.state.schema;
const props = {
...this.schemaProps,
theme: this.props.theme,
locale: this.props.locale,
affixHeader: false,
affixFooter: false
};
if (this.props.viewMode === 'mobile' && !isMobile()) {
return (
<iframe
width="375"
height="100%"
frameBorder={0}
className="mobile-frame"
ref={this.iframeRef}
// @ts-ignore
src={__uri('../mobile.html')}
></iframe>
);
}
return render(schema, props, this.env);
}
handleChange(value) {
this.setState({
schemaCode: value
});
try {
const schema = JSON5.parse(value);
this.setState(
{
schema
},
() => {
this.updateIframe();
}
);
} catch (e) {
//ignore
}
}
handleMouseDown(e) {
this.startX = e.clientX;
this.startWidth = this.state.asideWidth;
// this.startPosition.y = e.clientY;
window.addEventListener('mouseup', this.handleMouseUp);
window.addEventListener('mousemove', this.handleMouseMove);
return false;
}
handleMouseMove(e) {
const diff = this.startX - e.clientX;
e.preventDefault();
this.setState({
asideWidth: Math.min(800, Math.max(200, this.startWidth + diff))
});
}
handleMouseUp() {
this.removeWindowEvents();
}
removeWindowEvents() {
window.removeEventListener('mouseup', this.handleMouseUp);
window.removeEventListener('mousemove', this.handleMouseMove);
}
toggleDrawer() {
this.setState({
isOpened: !this.state.isOpened
});
}
close() {
this.setState({
isOpened: false
});
}
editorDidMount = (editor, monaco) => {
this.editor = editor;
this.monaco = monaco;
let host = `${window.location.protocol}//${window.location.host}`;
// 如果在 gh-pages 里面
if (/^\/amis/.test(window.location.pathname)) {
host += '/amis';
}
const schemaUrl = `${host}/schema.json`;
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
schemas: [
{
uri: schemaUrl,
fileMatch: ['*']
}
],
validate: true,
enableSchemaRequest: true,
allowComments: true
});
};
// editorFactory = (containerElement, monaco, options) => {
// this.model = monaco.editor.createModel(
// this.state.schemaCode,
// 'json',
// monaco.Uri.parse(`isuda://schemas/page.json`)
// );
// return monaco.editor.create(containerElement, {
// autoIndent: true,
// formatOnType: true,
// formatOnPaste: true,
// selectOnLineNumbers: true,
// scrollBeyondLastLine: false,
// folding: true,
// minimap: {
// enabled: false
// },
// ...options,
// model: this.model
// });
// };
renderEditor() {
const {theme} = this.props;
return (
<CodeEditor
value={this.state.schemaCode}
onChange={this.handleChange}
options={{
lineNumbers: 'off'
}}
// editorFactory={this.editorFactory}
editorDidMount={this.editorDidMount}
language="json"
editorTheme={theme === 'dark' ? 'vs-dark' : 'vs'}
/>
);
}
render() {
const {vertical, mini, height, theme, classPrefix} = this.props;
if (mini) {
return (
<div className="Playgroud Playgroud--mini">
<a onClick={this.toggleDrawer} className="Playgroud-edit-btn">
编辑代码 <i className="fa fa-code p-l-xs"></i>
</a>
<Drawer
showCloseButton
closeOnOutside
resizable
theme={theme}
overlay={false}
position="right"
show={this.state.isOpened}
onHide={this.close}
>
<div className={`${classPrefix}Drawer-header`}>
编辑代码支持编辑实时预览
</div>
<div className={`${classPrefix}Drawer-body no-padder`}>
{this.renderEditor()}
</div>
</Drawer>
<div style={{minHeight: height}} className="Playgroud-preview">
{this.renderPreview()}
</div>
</div>
);
} else if (vertical) {
return (
<div className="Playgroud">
<div style={{minHeight: height}} className="Playgroud-preview">
{this.renderPreview()}
</div>
<div className="Playgroud-code">{this.renderEditor()}</div>
</div>
);
}
return (
<div
style={{
position: 'absolute',
top: 50,
bottom: 0
}}
>
<div className="hbox">
<div className="col pos-rlt">
<div className="scroll-y h-full pos-abt w-full b-b">
{this.renderPreview()}
</div>
</div>
<div
className="col bg-light lter b-l bg-auto pos-rlt"
style={{width: this.state.asideWidth}}
>
<div className="resizer" onMouseDown={this.handleMouseDown} />
{this.renderEditor()}
</div>
</div>
</div>
);
}
}