@kui-shell/plugin-client-common
Version:
Kui plugin that offers stylesheets
258 lines • 11.6 kB
JavaScript
/*
* Copyright 2020 The Kubernetes Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { EventEmitter } from 'events';
import { i18n } from '@kui-shell/core/mdist/api/i18n';
import { TextContent } from '@patternfly/react-core/dist/esm/components/Text/TextContent';
import Card from '../spi/Card';
import { MutabilityContext } from '../Client/MutabilityContext';
import { CurrentMarkdownTab } from './Markdown/components/tabbed';
const Markdown = React.lazy(() => import('./Markdown'));
const SimpleMarkdown = React.lazy(() => import('./Markdown/Simple'));
const Button = React.lazy(() => import('../spi/Button'));
const SimpleEditor = React.lazy(() => import('./Editor/SimpleEditor'));
const strings = i18n('plugin-client-common');
/** Allows decoupled edit/preview */
const events = new EventEmitter();
/** Requests for current textValue */
function getChannel(props) {
return `/get/${props.receive || props.send}`;
}
/** Broadcast current textValue */
function editChannel(props) {
return `/edit/${props.receive || props.send}`;
}
export function onCommentaryEdit(channel, cb) {
const props = { receive: channel };
events.on(editChannel(props), cb);
events.emit(getChannel(props), cb);
}
export function offCommentaryEdit(channel, cb) {
events.off(editChannel({ receive: channel }), cb);
}
class Commentary extends React.PureComponent {
constructor(props) {
super(props);
this.cleaners = [];
this.onGet = (cb) => cb(this.state.textValue);
this.onEdit = (textValue) => {
this.setState({ textValue });
};
this._onCancel = this.onCancel.bind(this);
this._onRevert = this.onRevert.bind(this);
this._onDone = this.onDone.bind(this);
this._setEdit = this.setEdit.bind(this);
this._onContentChange = this.onContentChange.bind(this);
this._onSaveFromEditor = this.onSaveFromEditor.bind(this);
this._onCancelFromEditor = this.onCancelFromEditor.bind(this);
const textValue = this.initialTextValue();
this.state = {
initialActiveKey: props.activeKey,
isEdit: props.edit === false ? false : props.edit || textValue.length === 0,
textValue,
lastAppliedTextValue: textValue
};
}
static getDerivedStateFromProps(props, state) {
if (props.previousActiveKey !== undefined && props.activeKey === state.initialActiveKey) {
events.emit(editChannel(props), state.textValue, props.filepath);
}
return state;
}
componentDidMount() {
this.initCouplingEvents();
}
componentWillUnmount() {
this.cleaners.forEach(_ => _());
if (this.props.send) {
// broadcast clear
events.emit(editChannel(this.props), '');
}
}
componentDidUpdate(_, prevState) {
if (this.props.send) {
// broadcast new textValue if either:
// a. different textValue
// b. there has been a tab switch above us (i.e. in a contanining component)
if (prevState.textValue !== this.state.textValue ||
(this.props.previousActiveKey !== undefined && this.props.activeKey === this.state.initialActiveKey)) {
events.emit(editChannel(this.props), this.state.textValue, this.props.filepath);
}
}
}
/** Are we a coupled view, i.e. split edit/preview? */
get isCoupled() {
return this.props.send || this.props.receive;
}
/**
* Register either as a producer or consumer of edit events. Allows
* for decoupled edit/preview views.
*
*/
initCouplingEvents() {
if (this.props.receive) {
// this is the preview side of the coupling
events.on(editChannel(this.props), this.onEdit);
this.cleaners.push(() => events.off(editChannel(this.props), this.onEdit));
// emit an initial request for the content
setTimeout(() => events.emit(getChannel(this.props), this.onEdit));
}
else if (this.props.send) {
// this is the edit side of the coupling
events.on(getChannel(this.props), this.onGet);
this.cleaners.push(() => events.off(getChannel(this.props), this.onGet));
// emit an initial value for the content
setTimeout(() => events.emit(editChannel(this.props), this.state.textValue, this.props.filepath));
}
}
/** update state to cancel any edits and close the editor */
onCancel(evt) {
this.onRevert(evt, false);
this.removeOurselvesIfEmpty();
}
/** cancel button */
cancel() {
return (React.createElement(Button, { variant: "secondary", isSmall: true, className: "kui--tab-navigatable kui--commentary-button kui--commentary-cancel-button", onClick: this._onCancel }, strings('Cancel')));
}
/** Update state to cancel any updates, but leave editor open */
onRevert(evt, isEdit = true) {
if (evt) {
// so that the event doesn't propagate to the onClick on the Card itself
evt.stopPropagation();
}
this.setState(curState => {
// switch back to the lastAppliedTextValue
const textValue = curState.lastAppliedTextValue;
if (this.props.willUpdateResponse) {
this.props.willUpdateResponse(textValue);
}
return { isEdit, textValue };
});
}
/** revert button */
revert() {
return (React.createElement(Button, { variant: "tertiary", isSmall: true, className: "kui--tab-navigatable kui--commentary-button kui--commentary-revert-button", onClick: this._onRevert }, strings('Revert')));
}
/** If the user clicks Done or Cancel and there is no text, remove ourselves */
removeOurselvesIfEmpty() {
if (this.state.textValue === '') {
if (this.props.willRemove) {
this.props.willRemove();
}
return true;
}
else {
return false;
}
}
/** Update state to reflect lastAppliedTextValue, and close the editor */
onDone(evt) {
if (evt) {
// so that the event doesn't propagate to the onClick on the Card itself
evt.stopPropagation();
}
if (!this.removeOurselvesIfEmpty()) {
this.setState(curState => {
this.props.willUpdateCommand(`# ${curState.textValue.replace(/\n/g, '\\n').replace(/\t/g, '\\t')}`);
return { isEdit: false, lastAppliedTextValue: curState.textValue };
});
}
}
/** done button removes the editor */
done() {
return (React.createElement(Button, { isSmall: true, className: "kui--tab-navigatable kui--commentary-button kui--commentary-done-button", onClick: this._onDone }, strings('Done')));
}
/** toolbar hosts editor actions */
toolbar() {
return (React.createElement("div", { className: "kui--commentary-editor-toolbar fill-container flush-right" },
this.done(),
"\u00A0",
this.cancel(),
"\u00A0",
this.revert()));
}
/** Enter isEdit mode */
setEdit() {
this.setState({ isEdit: true });
}
preview() {
if (this.props.preview !== false) {
if (this.props.simple) {
return (React.createElement(SimpleMarkdown, { nested: true, execUUID: this.props.execUUID, filepath: this.props.filepath, baseUrl: this.props.baseUrl, source: this.state.textValue, tab: this.props.tab }));
}
else {
return (React.createElement(Markdown, { nested: true, execUUID: this.props.execUUID, filepath: this.props.filepath, source: this.state.textValue, codeBlockResponses: this.props.codeBlockResponses, baseUrl: this.props.baseUrl, snippetBasePath: this.props.snippetBasePath, tab: this.props.tab }));
}
}
}
card() {
return (React.createElement(MutabilityContext.Consumer, null, value => (React.createElement("span", { className: "kui--commentary-card", onDoubleClick: !value.editable ? undefined : this._setEdit },
React.createElement(Card, Object.assign({}, this.props, { "data-is-editing": this.state.isEdit || undefined, header: this.state.isEdit && this.props.header !== false && strings('Editing Comment as Markdown'), footer: this.state.isEdit && !this.isCoupled && this.toolbar() }),
this.preview(),
this.state.isEdit && this.editor())))));
}
/** Percolate `SimpleEditor` edits up to the Preview view */
onContentChange(value) {
this.setState(curState => {
if (!curState.isEdit) {
// then we've already exited edit mode, ignore this content
// change event
return null;
}
else {
return { textValue: value };
}
});
if (this.props.willUpdateResponse) {
this.props.willUpdateResponse(value);
}
}
/** User has requested to save changes via keyboard shortcut, from within `SimpleEditor` */
onSaveFromEditor(value) {
this.onContentChange(value);
this.onDone();
}
/** User has requested to cancel changes via keyboard shortcut, from within `SimpleEditor` */
onCancelFromEditor() {
this.onCancel();
}
/** @return the initial content to display, before any editing */
initialTextValue() {
return this.props.children || '';
}
editor() {
return (React.createElement(React.Suspense, { fallback: React.createElement("div", null) },
React.createElement(SimpleEditor, { tabUUID: this.props.tab.uuid, content: this.state.textValue, className: "kui--source-ref-editor kui--commentary-editor", readonly: false, simple: true, wordWrap: "on", onSave: this._onSaveFromEditor, onCancel: !this.isCoupled && this._onCancelFromEditor, onContentChange: this._onContentChange, contentType: "markdown", scrollIntoView: false })));
}
render() {
this.props.onRender();
return (React.createElement("div", { className: "kui--commentary", "data-is-editing": this.state.isEdit || undefined, "data-no-header": this.props.header === false || undefined }, this.card()));
}
}
export default class CommentaryExternal extends React.PureComponent {
render() {
return (React.createElement(CurrentMarkdownTab.Consumer, null, config => React.createElement(Commentary, Object.assign({}, this.props, config))));
}
}
export class ReactCommentary extends React.PureComponent {
render() {
return (React.createElement("div", { className: "kui--commentary" },
React.createElement("span", { className: "kui--commentary-card" },
React.createElement(Card, null,
React.createElement(TextContent, null, this.props.children)))));
}
}
//# sourceMappingURL=Commentary.js.map