UNPKG

@kui-shell/plugin-client-common

Version:

Kui plugin that offers stylesheets

258 lines 11.6 kB
/* * 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