| // 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. |
| |
| define([ |
| '../../app', |
| '../../core/api', |
| 'react', |
| 'react-dom', |
| './stores', |
| './actions', |
| '../fauxton/components.react', |
| '../documents/helpers', |
| '../../../assets/js/plugins/beautify', |
| 'react-bootstrap', |
| 'react-addons-css-transition-group', |
| 'brace' |
| ], |
| function (app, FauxtonAPI, React, ReactDOM, Stores, Actions, FauxtonComponents, Helpers, |
| beautifyHelper, ReactBootstrap, ReactCSSTransitionGroup, ace) { |
| |
| var componentStore = Stores.componentStore; |
| var Modal = ReactBootstrap.Modal; |
| |
| var BadgeList = React.createClass({ |
| |
| propTypes: { |
| elements: React.PropTypes.array.isRequired, |
| removeBadge: React.PropTypes.func.isRequired |
| }, |
| |
| getDefaultProps: function () { |
| return { |
| getLabel: function (el) { |
| return el; |
| }, |
| |
| getId: function (el) { |
| return el; |
| } |
| |
| }; |
| }, |
| |
| getBadges: function () { |
| return this.props.elements.map(function (el, i) { |
| return <Badge |
| label={this.props.getLabel(el)} |
| key={i} |
| id={el} |
| remove={this.removeBadge} />; |
| }.bind(this)); |
| }, |
| |
| removeBadge: function (label, el) { |
| this.props.removeBadge(label, el); |
| }, |
| |
| render: function () { |
| return ( |
| <ul className="component-badgelist"> |
| {this.getBadges()} |
| </ul> |
| ); |
| } |
| }); |
| |
| var Badge = React.createClass({ |
| propTypes: { |
| label: React.PropTypes.string.isRequired, |
| remove: React.PropTypes.func.isRequired |
| }, |
| |
| remove: function (e) { |
| e.preventDefault(); |
| this.props.remove(this.props.label, this.props.id); |
| }, |
| |
| render: function () { |
| return ( |
| <li className="component-badge"> |
| <span className="label label-info">{this.props.label}</span> |
| <a |
| href="#" |
| className="label label-info js-remove-filter" |
| onClick={this.remove} data-bypass="true" |
| > |
| × |
| </a> |
| </li> |
| ); |
| } |
| }); |
| |
| var ToggleHeaderButton = React.createClass({ |
| getDefaultProps: function () { |
| return { |
| innerClasses: '', |
| fonticon: '', |
| containerClasses: '', |
| selected: false, |
| title: '', |
| disabled: false, |
| toggleCallback: null, |
| text: '', |
| iconDefaultClass: 'icon' |
| }; |
| }, |
| |
| render: function () { |
| var iconClasses = this.props.iconDefaultClass + ' ' + this.props.fonticon + ' ' + this.props.innerClasses, |
| containerClasses = 'button ' + this.props.containerClasses; |
| |
| if (this.props.selected) { |
| containerClasses = containerClasses + ' js-headerbar-togglebutton-selected'; |
| } |
| |
| return ( |
| <button |
| title={this.props.title} |
| disabled={this.props.disabled} |
| onClick={this.props.toggleCallback} |
| className={containerClasses} |
| > |
| <i className={iconClasses}></i><span>{this.props.text}</span> |
| </button> |
| ); |
| } |
| }); |
| |
| |
| var BulkActionComponent = React.createClass({ |
| |
| propTypes: { |
| hasSelectedItem: React.PropTypes.bool.isRequired, |
| removeItem: React.PropTypes.func.isRequired, |
| selectAll: React.PropTypes.func, |
| toggleSelect: React.PropTypes.func.isRequired, |
| isChecked: React.PropTypes.bool.isRequired, |
| disabled: React.PropTypes.bool |
| }, |
| |
| getDefaultProps: function () { |
| return { |
| disabled: false, |
| title: 'Select rows that can be...', |
| bulkIcon: 'fonticon-trash', |
| buttonTitle: 'Delete all selected', |
| dropdownContentText: 'Deleted', |
| enableOverlay: false |
| }; |
| }, |
| |
| render: function () { |
| return ( |
| <div className="bulk-action-component"> |
| <div className="bulk-action-component-selector-group"> |
| {this.getMasterSelector()} |
| {this.getMultiSelectOptions()} |
| </div> |
| </div> |
| ); |
| }, |
| |
| getMultiSelectOptions: function () { |
| if (!this.props.hasSelectedItem) { |
| return null; |
| } |
| |
| return ( |
| <button |
| onClick={this.props.removeItem} |
| className={'fonticon ' + this.props.bulkIcon} |
| title={this.props.buttonTitle} /> |
| ); |
| }, |
| |
| getPopupContent: function () { |
| return ( |
| <ul className="bulk-action-component-popover-actions"> |
| <li onClick={this.selectAll} > |
| <i className="icon fonticon-cancel"></i> {this.props.dropdownContentText} |
| </li> |
| </ul> |
| ); |
| }, |
| |
| selectAll: function () { |
| this.refs.bulkActionPopover.hide(); |
| this.props.selectAll(); |
| }, |
| |
| getOverlay: function () { |
| return ( |
| <ReactBootstrap.OverlayTrigger |
| ref="bulkActionPopover" |
| trigger="click" |
| placement="bottom" |
| rootClose={true} |
| overlay={ |
| <ReactBootstrap.Popover id="bulk-action-component-popover" title={this.props.title}> |
| {this.getPopupContent()} |
| </ReactBootstrap.Popover> |
| }> |
| <div className="arrow-button"> |
| <i className="fonticon fonticon-play"></i> |
| </div> |
| </ReactBootstrap.OverlayTrigger> |
| ); |
| }, |
| |
| getMasterSelector: function () { |
| return ( |
| <div className="bulk-action-component-panel"> |
| <input type="checkbox" |
| checked={this.props.isChecked} |
| onChange={this.props.toggleSelect} |
| disabled={this.props.disabled} /> |
| {this.props.enableOverlay ? <div className="separator"></div> : null} |
| {this.props.enableOverlay ? this.getOverlay() : null} |
| </div> |
| ); |
| }, |
| |
| }); |
| |
| var StyledSelect = React.createClass({ |
| propTypes: { |
| selectValue: React.PropTypes.string.isRequired, |
| selectId: React.PropTypes.string.isRequired, |
| selectChange: React.PropTypes.func.isRequired |
| }, |
| |
| render: function () { |
| return ( |
| <div className="styled-select"> |
| <label htmlFor={this.props.selectId}> |
| <i className="fonticon-down-dir"></i> |
| <select |
| value={this.props.selectValue} |
| id={this.props.selectId} |
| className={this.props.selectValue} |
| onChange={this.props.selectChange} |
| > |
| {this.props.selectContent} |
| </select> |
| </label> |
| </div> |
| ); |
| } |
| }); |
| |
| |
| /** |
| * A pre-packaged JS editor panel for use on the Edit Index / Mango pages. Includes options for a title, zen mode |
| * icon and beautify button. |
| */ |
| var CodeEditorPanel = React.createClass({ |
| getDefaultProps: function () { |
| return { |
| id: 'code-editor', |
| className: '', |
| defaultCode: '', |
| title: '', |
| docLink: '', |
| allowZenMode: true, |
| blur: function () {} |
| }; |
| }, |
| |
| getInitialState: function () { |
| return this.getStoreState(); |
| }, |
| |
| getStoreState: function () { |
| return { |
| zenModeEnabled: false, |
| code: this.props.defaultCode |
| }; |
| }, |
| |
| componentWillReceiveProps: function (nextProps) { |
| if (nextProps.defaultCode !== this.props.defaultCode) { |
| this.setState({ code: nextProps.defaultCode }); |
| } |
| }, |
| |
| // list of JSHINT errors to ignore: gets around problem of anonymous functions not being valid |
| ignorableErrors: [ |
| 'Missing name in function declaration.', |
| "['{a}'] is better written in dot notation." |
| ], |
| |
| getZenModeIcon: function () { |
| if (this.props.allowZenMode) { |
| return <span className="fonticon fonticon-resize-full zen-editor-icon" title="Enter Zen mode" onClick={this.enterZenMode}></span>; |
| } |
| }, |
| |
| getDocIcon: function () { |
| if (this.props.docLink) { |
| return ( |
| <a className="help-link" |
| data-bypass="true" |
| href={this.props.docLink} |
| target="_blank" |
| > |
| <i className="icon-question-sign"></i> |
| </a> |
| ); |
| } |
| }, |
| |
| getZenModeOverlay: function () { |
| if (this.state.zenModeEnabled) { |
| return ( |
| <ZenModeOverlay |
| defaultCode={this.state.code} |
| mode={this.props.mode} |
| ignorableErrors={this.ignorableErrors} |
| onExit={this.exitZenMode} /> |
| ); |
| } |
| }, |
| |
| enterZenMode: function () { |
| this.setState({ |
| zenModeEnabled: true, |
| code: this.refs.codeEditor.getValue() |
| }); |
| }, |
| |
| exitZenMode: function (content) { |
| this.setState({ zenModeEnabled: false }); |
| this.getEditor().setValue(content); |
| }, |
| |
| getEditor: function () { |
| return this.refs.codeEditor; |
| }, |
| |
| getValue: function () { |
| return this.getEditor().getValue(); |
| }, |
| |
| beautify: function (code) { |
| this.setState({ code: code }); |
| this.getEditor().setValue(code); |
| }, |
| |
| update: function () { |
| this.getEditor().setValue(this.state.code); |
| }, |
| |
| render: function () { |
| var classes = 'control-group'; |
| if (this.props.className) { |
| classes += ' ' + this.props.className; |
| } |
| return ( |
| <div className={classes}> |
| <label> |
| <span>{this.props.title}</span> |
| {this.getDocIcon()} |
| {this.getZenModeIcon()} |
| </label> |
| <CodeEditor |
| id={this.props.id} |
| ref="codeEditor" |
| mode="javascript" |
| defaultCode={this.state.code} |
| showGutter={true} |
| ignorableErrors={this.ignorableErrors} |
| setHeightToLineCount={true} |
| blur={this.props.blur} |
| /> |
| <Beautify code={this.state.code} beautifiedCode={this.beautify} /> |
| {this.getZenModeOverlay()} |
| </div> |
| ); |
| } |
| }); |
| |
| |
| require('brace/mode/javascript'); |
| require('brace/mode/json'); |
| require('brace/theme/idle_fingers'); |
| |
| var CodeEditor = React.createClass({ |
| getDefaultProps: function () { |
| return { |
| id: 'code-editor', |
| mode: 'javascript', |
| theme: 'idle_fingers', |
| fontSize: 13, |
| |
| // this sets the default value for the editor. On the fly changes are stored in state in this component only. To |
| // change the editor content after initial construction use CodeEditor.setValue() |
| defaultCode: '', |
| |
| showGutter: true, |
| highlightActiveLine: true, |
| showPrintMargin: false, |
| autoScrollEditorIntoView: true, |
| autoFocus: false, |
| stringEditModalEnabled: false, |
| |
| // these two options create auto-resizeable code editors, with a maximum number of lines |
| setHeightToLineCount: false, |
| maxLines: 10, |
| |
| // optional editor key commands (e.g. specific save action) |
| editorCommands: [], |
| |
| // notifies users that there is unsaved changes in the editor when navigating away from the page |
| notifyUnsavedChanges: false, |
| |
| // an optional array of ignorable Ace errors. Lets us filter out errors based on context |
| ignorableErrors: [], |
| |
| // un-Reacty, but the code editor is a self-contained component and it's helpful to be able to tie into |
| // editor specific events like content changes and leaving the editor |
| change: function () {}, |
| blur: function () {} |
| }; |
| }, |
| |
| getInitialState: function () { |
| return { |
| originalCode: this.props.defaultCode, |
| |
| // these are all related to the (optional) string edit modal |
| stringEditModalVisible: false, |
| stringEditIconVisible: false, |
| stringEditIconStyle: {}, |
| stringEditModalValue: '' |
| }; |
| }, |
| |
| hasChanged: function () { |
| return !_.isEqual(this.state.originalCode, this.getValue()); |
| }, |
| |
| clearChanges: function () { |
| this.setState({ |
| originalCode: this.getValue() |
| }); |
| }, |
| |
| setupAce: function (props, shouldUpdateCode) { |
| this.editor = ace.edit(ReactDOM.findDOMNode(this.refs.ace)); |
| |
| // suppresses an Ace editor error |
| this.editor.$blockScrolling = Infinity; |
| |
| if (shouldUpdateCode) { |
| this.setValue(props.defaultCode); |
| } |
| |
| this.editor.setShowPrintMargin(props.showPrintMargin); |
| this.editor.autoScrollEditorIntoView = props.autoScrollEditorIntoView; |
| |
| this.editor.setOption('highlightActiveLine', this.props.highlightActiveLine); |
| |
| if (this.props.setHeightToLineCount) { |
| this.setHeightToLineCount(); |
| } |
| |
| if (this.props.ignorableErrors) { |
| this.removeIgnorableAnnotations(); |
| } |
| |
| this.addCommands(); |
| this.editor.getSession().setMode('ace/mode/' + props.mode); |
| this.editor.setTheme('ace/theme/' + props.theme); |
| this.editor.setFontSize(props.fontSize); |
| this.editor.getSession().setTabSize(2); |
| this.editor.getSession().setUseSoftTabs(true); |
| |
| if (this.props.autoFocus) { |
| this.editor.focus(); |
| } |
| }, |
| |
| addCommands: function () { |
| _.each(this.props.editorCommands, function (command) { |
| this.editor.commands.addCommand(command); |
| }, this); |
| }, |
| |
| setupEvents: function () { |
| this.editor.on('blur', _.bind(this.onBlur, this)); |
| this.editor.on('change', _.bind(this.onContentChange, this)); |
| |
| if (this.props.stringEditModalEnabled) { |
| this.editor.on('changeSelection', _.bind(this.showHideEditStringGutterIcon, this)); |
| this.editor.getSession().on('changeBackMarker', _.bind(this.showHideEditStringGutterIcon, this)); |
| this.editor.getSession().on('changeScrollTop', _.bind(this.updateEditStringGutterIconPosition, this)); |
| } |
| |
| if (this.props.notifyUnsavedChanges) { |
| $(window).on('beforeunload.editor_' + this.props.id, _.bind(this.quitWarningMsg)); |
| FauxtonAPI.beforeUnload('editor_' + this.props.id, _.bind(this.quitWarningMsg, this)); |
| } |
| }, |
| |
| onBlur: function () { |
| this.props.blur(this.getValue()); |
| }, |
| |
| onContentChange: function () { |
| if (this.props.setHeightToLineCount) { |
| this.setHeightToLineCount(); |
| } |
| this.props.change(this.getValue()); |
| }, |
| |
| quitWarningMsg: function () { |
| if (this.hasChanged()) { |
| return 'Your changes have not been saved. Click Cancel to return to the document, or OK to proceed.'; |
| } |
| }, |
| |
| removeEvents: function () { |
| if (this.props.notifyUnsavedChanges) { |
| $(window).off('beforeunload.editor_' + this.props.id); |
| FauxtonAPI.removeBeforeUnload('editor_' + this.props.id); |
| } |
| }, |
| |
| setHeightToLineCount: function () { |
| var numLines = this.editor.getSession().getDocument().getLength(); |
| var maxLines = (numLines > this.props.maxLines) ? this.props.maxLines : numLines; |
| this.editor.setOptions({ |
| maxLines: maxLines |
| }); |
| }, |
| |
| componentDidMount: function () { |
| this.setupAce(this.props, true); |
| this.setupEvents(); |
| |
| if (this.props.autoFocus) { |
| this.editor.focus(); |
| } |
| }, |
| |
| componentWillUnmount: function () { |
| this.removeEvents(); |
| this.editor.destroy(); |
| }, |
| |
| componentWillReceiveProps: function (nextProps) { |
| this.setupAce(nextProps, false); |
| }, |
| |
| getAnnotations: function () { |
| return this.editor.getSession().getAnnotations(); |
| }, |
| |
| isIgnorableError: function (msg) { |
| return _.contains(this.props.ignorableErrors, msg); |
| }, |
| |
| removeIgnorableAnnotations: function () { |
| var isIgnorableError = this.isIgnorableError; |
| this.editor.getSession().on('changeAnnotation', function () { |
| var annotations = this.editor.getSession().getAnnotations(); |
| var newAnnotations = _.reduce(annotations, function (annotations, error) { |
| if (!isIgnorableError(error.raw)) { |
| annotations.push(error); |
| } |
| return annotations; |
| }, []); |
| |
| if (annotations.length !== newAnnotations.length) { |
| this.editor.getSession().setAnnotations(newAnnotations); |
| } |
| }.bind(this)); |
| }, |
| |
| showHideEditStringGutterIcon: function (e) { |
| if (this.hasErrors() || !this.parseLineForStringMatch()) { |
| this.setState({ stringEditIconVisible: false }); |
| return false; |
| } |
| |
| this.setState({ |
| stringEditIconVisible: true, |
| stringEditIconStyle: { |
| top: this.getGutterIconPosition() |
| } |
| }); |
| |
| return true; |
| }, |
| |
| updateEditStringGutterIconPosition: function () { |
| if (!this.state.stringEditIconVisible) { |
| return; |
| } |
| this.setState({ |
| stringEditIconStyle: { |
| top: this.getGutterIconPosition() |
| } |
| }); |
| }, |
| |
| getGutterIconPosition: function () { |
| var rowHeight = this.getRowHeight(); |
| var scrollTop = this.editor.session.getScrollTop(); |
| var positionFromTop = (rowHeight * this.documentToScreenRow(this.getSelectionStart().row)) - scrollTop; |
| return positionFromTop + 'px'; |
| }, |
| |
| parseLineForStringMatch: function () { |
| var selStart = this.getSelectionStart().row; |
| var selEnd = this.getSelectionEnd().row; |
| |
| // one JS(ON) string can't span more than one line - we edit one string, so ensure we don't select several lines |
| if (selStart >= 0 && selEnd >= 0 && selStart === selEnd && this.isRowExpanded(selStart)) { |
| var editLine = this.getLine(selStart), |
| editMatch = editLine.match(/^([ \t]*)("[a-zA-Z0-9_]*["|']: )?(["|'].*",?[ \t]*)$/); |
| |
| if (editMatch) { |
| return editMatch; |
| } |
| } |
| return false; |
| }, |
| |
| openStringEditModal: function () { |
| var matches = this.parseLineForStringMatch(); |
| var string = matches[3]; |
| var lastChar = string.length - 1; |
| if (string.substring(string.length - 1) === ',') { |
| lastChar = string.length - 2; |
| } |
| string = string.substring(1, lastChar); |
| |
| this.setState({ |
| stringEditModalVisible: true, |
| stringEditModalValue: string |
| }); |
| }, |
| |
| saveStringEditModal: function (newString) { |
| // replace the string on the selected line |
| var line = this.parseLineForStringMatch(); |
| var indent = line[1] || '', |
| key = line[2] || '', |
| originalString = line[3], |
| comma = ''; |
| if (originalString.substring(originalString.length - 1) === ',') { |
| comma = ','; |
| } |
| this.replaceCurrentLine(indent + key + JSON.stringify(newString) + comma + '\n'); |
| this.closeStringEditModal(); |
| }, |
| |
| closeStringEditModal: function () { |
| this.setState({ |
| stringEditModalVisible: false |
| }); |
| }, |
| |
| hasErrors: function () { |
| return !_.every(this.getAnnotations(), function (error) { |
| return this.isIgnorableError(error.raw); |
| }, this); |
| }, |
| |
| setReadOnly: function (readonly) { |
| this.editor.setReadOnly(readonly); |
| }, |
| |
| setValue: function (code, lineNumber) { |
| lineNumber = lineNumber ? lineNumber : -1; |
| this.editor.setValue(code, lineNumber); |
| }, |
| |
| getValue: function () { |
| return this.editor.getValue(); |
| }, |
| |
| getEditor: function () { |
| return this; |
| }, |
| |
| getLine: function (lineNum) { |
| return this.editor.session.getLine(lineNum); |
| }, |
| |
| getSelectionStart: function () { |
| return this.editor.getSelectionRange().start; |
| }, |
| |
| getSelectionEnd: function () { |
| return this.editor.getSelectionRange().end; |
| }, |
| |
| getRowHeight: function () { |
| return this.editor.renderer.layerConfig.lineHeight; |
| }, |
| |
| isRowExpanded: function (row) { |
| return !this.editor.getSession().isRowFolded(row); |
| }, |
| |
| documentToScreenRow: function (row) { |
| return this.editor.getSession().documentToScreenRow(row, 0); |
| }, |
| |
| replaceCurrentLine: function (replacement) { |
| this.editor.getSelection().selectLine(); |
| this.editor.insert(replacement); |
| this.editor.getSelection().moveCursorUp(); |
| }, |
| |
| render: function () { |
| return ( |
| <div> |
| <div ref="ace" className="js-editor" id={this.props.id}></div> |
| <button ref="stringEditIcon" className="btn string-edit" title="Edit string" disabled={!this.state.stringEditIconVisible} |
| style={this.state.stringEditIconStyle} onClick={this.openStringEditModal}> |
| <i className="icon icon-edit"></i> |
| </button> |
| <StringEditModal |
| ref="stringEditModal" |
| visible={this.state.stringEditModalVisible} |
| value={this.state.stringEditModalValue} |
| onSave={this.saveStringEditModal} |
| onClose={this.closeStringEditModal} /> |
| </div> |
| ); |
| } |
| }); |
| |
| |
| // this appears when the cursor is over a string. It shows an icon in the gutter that opens the modal. |
| var StringEditModal = React.createClass({ |
| |
| propTypes: { |
| value: React.PropTypes.string.isRequired, |
| visible: React.PropTypes.bool.isRequired, |
| onClose: React.PropTypes.func.isRequired, |
| onSave: React.PropTypes.func.isRequired |
| }, |
| |
| getDefaultProps: function () { |
| return { |
| visible: false, |
| onClose: function () { }, |
| onSave: function () { } |
| }; |
| }, |
| |
| componentDidMount: function () { |
| if (!this.props.visible) { |
| return; |
| } |
| this.initEditor(this.props.value); |
| }, |
| |
| componentDidUpdate: function (prevProps) { |
| if (!this.props.visible) { |
| return; |
| } |
| var val = ''; |
| if (!prevProps.visible && this.props.visible) { |
| val = Helpers.parseJSON(this.props.value); |
| } |
| |
| this.initEditor(val); |
| }, |
| |
| initEditor: function (val) { |
| this.editor = ace.edit(ReactDOM.findDOMNode(this.refs.stringEditor)); |
| this.editor.$blockScrolling = Infinity; // suppresses an Ace editor error |
| this.editor.setShowPrintMargin(false); |
| this.editor.setOption('highlightActiveLine', true); |
| this.editor.setTheme('ace/theme/idle_fingers'); |
| this.editor.setValue(val, -1); |
| }, |
| |
| closeModal: function () { |
| this.props.onClose(); |
| }, |
| |
| save: function () { |
| this.props.onSave(this.editor.getValue()); |
| }, |
| |
| render: function () { |
| return ( |
| <Modal dialogClassName="string-editor-modal" show={this.props.visible} onHide={this.closeModal}> |
| <Modal.Header closeButton={true}> |
| <Modal.Title>Edit text <span id="string-edit-header"></span></Modal.Title> |
| </Modal.Header> |
| <Modal.Body> |
| <div id="modal-error" className="hide alert alert-error"/> |
| <div id="string-editor-wrapper"><div ref="stringEditor" className="doc-code"></div></div> |
| </Modal.Body> |
| <Modal.Footer> |
| <button className="cancel-button btn" onClick={this.closeModal}><i className="icon fonticon-circle-x"></i> Cancel</button> |
| <button id="string-edit-save-btn" onClick={this.save} className="btn btn-success save"> |
| <i className="fonticon-circle-check"></i> Save |
| </button> |
| </Modal.Footer> |
| </Modal> |
| ); |
| } |
| }); |
| |
| |
| // Zen mode editing has very few options: |
| // - It covers the full screen, hiding everything else |
| // - Two themes: light & dark (choice stored in local storage) |
| // - No save option, but has a 1-1 map with a <CodeEditor /> element which gets updated when the user leaves |
| // - [Escape] closes the mode, as does clicking the shrink icon at the top right |
| var ZenModeOverlay = React.createClass({ |
| getDefaultProps: function () { |
| return { |
| mode: 'javascript', |
| defaultCode: '', |
| ignorableErrors: [], |
| onExit: null, |
| highlightActiveLine: false |
| }; |
| }, |
| |
| themes: { |
| dark: 'idle_fingers', |
| light: 'dawn' |
| }, |
| |
| getInitialState: function () { |
| return this.getStoreState(); |
| }, |
| |
| getStoreState: function () { |
| return { |
| theme: this.getZenTheme(), |
| code: this.props.defaultCode |
| }; |
| }, |
| |
| getZenTheme: function () { |
| var selectedTheme = app.utils.localStorageGet('zenTheme'); |
| return _.isUndefined(selectedTheme) ? 'dark' : selectedTheme; |
| }, |
| |
| onChange: function () { |
| this.setState(this.getStoreState()); |
| }, |
| |
| componentDidMount: function () { |
| $(ReactDOM.findDOMNode(this.refs.exit)).tooltip({ placement: 'left' }); |
| $(ReactDOM.findDOMNode(this.refs.theme)).tooltip({ placement: 'left' }); |
| }, |
| |
| exitZenMode: function () { |
| this.props.onExit(this.getValue()); |
| }, |
| |
| getValue: function () { |
| return this.refs.ace.getValue(); |
| }, |
| |
| toggleTheme: function () { |
| var newTheme = (this.state.theme === 'dark') ? 'light' : 'dark'; |
| this.setState({ |
| theme: newTheme, |
| code: this.getValue() |
| }); |
| app.utils.localStorageSet('zenTheme', newTheme); |
| }, |
| |
| setValue: function (code, lineNumber) { |
| lineNumber = lineNumber ? lineNumber : -1; |
| this.editor.setValue(code, lineNumber); |
| }, |
| |
| render: function () { |
| var classes = 'full-page-editor-modal-wrapper zen-theme-' + this.state.theme; |
| |
| var editorCommands = [{ |
| name: 'close', |
| bindKey: { win: 'ESC', mac: 'ESC' }, |
| exec: this.exitZenMode |
| }]; |
| |
| return ( |
| <div className={classes}> |
| <div className="zen-mode-controls"> |
| <ul> |
| <li> |
| <span ref="exit" |
| className="fonticon fonticon-resize-small js-exit-zen-mode" |
| data-toggle="tooltip" |
| data-container=".zen-mode-controls .tooltips" |
| title="Exit zen mode (`esc`)" |
| onClick={this.exitZenMode}></span> |
| </li> |
| <li> |
| <span ref="theme" |
| className="fonticon fonticon-picture js-toggle-theme" |
| data-toggle="tooltip" |
| data-container=".zen-mode-controls .tooltips" |
| title="Switch zen theme" |
| onClick={this.toggleTheme}></span> |
| </li> |
| </ul> |
| <div className="tooltips"></div> |
| </div> |
| <CodeEditor |
| ref="ace" |
| autoFocus={true} |
| theme={this.themes[this.state.theme]} |
| defaultCode={this.props.defaultCode} |
| editorCommands={editorCommands} |
| ignorableErrors={this.props.ignorableErrors} |
| highlightActiveLine={this.props.highlightActiveLine} |
| /> |
| </div> |
| ); |
| } |
| }); |
| |
| |
| var Beautify = React.createClass({ |
| noOfLines: function () { |
| return this.props.code.split(/\r\n|\r|\n/).length; |
| }, |
| |
| canBeautify: function () { |
| return this.noOfLines() === 1; |
| }, |
| |
| addTooltip: function () { |
| if (this.canBeautify) { |
| $('.beautify-tooltip').tooltip({ placement: 'right' }); |
| } |
| }, |
| |
| componentDidMount: function () { |
| this.addTooltip(); |
| }, |
| |
| beautify: function (event) { |
| event.preventDefault(); |
| var beautifiedCode = beautifyHelper(this.props.code); |
| this.props.beautifiedCode(beautifiedCode); |
| $('.beautify-tooltip').tooltip('hide'); |
| }, |
| |
| render: function () { |
| if (!this.canBeautify()) { |
| return null; |
| } |
| |
| return ( |
| <button |
| onClick={this.beautify} |
| className="beautify beautify_map btn btn-primary btn-small beautify-tooltip" |
| type="button" |
| data-toggle="tooltip" |
| title="Reformat your minified code to make edits to it." |
| > |
| beautify this code |
| </button> |
| ); |
| } |
| }); |
| |
| var PaddedBorderedBox = React.createClass({ |
| render: function () { |
| return ( |
| <div className="bordered-box"> |
| <div className="padded-box"> |
| {this.props.children} |
| </div> |
| </div> |
| ); |
| } |
| }); |
| |
| var Document = React.createClass({ |
| propTypes: { |
| docIdentifier: React.PropTypes.string.isRequired, |
| docChecked: React.PropTypes.func.isRequired, |
| truncate: React.PropTypes.bool, |
| maxRows: React.PropTypes.number |
| }, |
| |
| getDefaultProps: function () { |
| return { |
| truncate: true, |
| maxRows: 500 |
| }; |
| }, |
| |
| onChange: function (e) { |
| e.preventDefault(); |
| this.props.docChecked(this.props.doc.id, this.props.doc._rev); |
| }, |
| |
| getUrlFragment: function () { |
| if (!this.props.children) { |
| return ''; |
| } |
| |
| return ( |
| <div className="doc-edit-symbol pull-right" title="Edit document"> |
| {this.props.children} |
| </div> |
| ); |
| }, |
| |
| getExtensionIcons: function () { |
| var extensions = FauxtonAPI.getExtensions('DocList:icons'); |
| return _.map(extensions, function (Extension, i) { |
| return (<Extension doc={this.props.doc} key={i} />); |
| }, this); |
| }, |
| |
| getCheckbox: function () { |
| if (!this.props.isDeletable) { |
| return <div className="checkbox-dummy"></div>; |
| } |
| |
| return ( |
| <div className="checkbox inline"> |
| <input |
| id={'checkbox-' + this.props.docIdentifier} |
| checked={this.props.checked} |
| data-checked={this.props.checked} |
| type="checkbox" |
| onChange={this.onChange} |
| className="js-row-select" /> |
| <label onClick={this.onChange} |
| className="label-checkbox-doclist" |
| htmlFor={'checkbox-' + this.props.docIdentifier} /> |
| </div> |
| ); |
| }, |
| |
| onDoubleClick: function (e) { |
| this.props.onDoubleClick(this.props.docIdentifier, this.props.doc, e); |
| }, |
| |
| getDocContent: function () { |
| if (_.isEmpty(this.props.docContent)) { |
| return null; |
| } |
| |
| // if need be, truncate the document |
| var content = this.props.docContent; |
| var isTruncated = false; |
| if (this.props.truncate) { |
| var result = Helpers.truncateDoc(this.props.docContent, this.props.maxRows); |
| isTruncated = result.isTruncated; |
| content = result.content; |
| } |
| |
| return ( |
| <div className="doc-data"> |
| <pre className="prettyprint">{content}</pre> |
| {isTruncated ? <div className="doc-content-truncated">(truncated)</div> : null} |
| </div> |
| ); |
| }, |
| |
| render: function () { |
| return ( |
| <div data-id={this.props.docIdentifier} onDoubleClick={this.onDoubleClick} className="doc-row"> |
| <div className="custom-inputs"> |
| {this.getCheckbox()} |
| </div> |
| <div className="doc-item"> |
| <header> |
| <span className="header-keylabel"> |
| {this.props.keylabel} |
| </span> |
| <span className="header-doc-id"> |
| {this.props.header ? '"' + this.props.header + '"' : null} |
| </span> |
| {this.getUrlFragment()} |
| <div className="doc-item-extension-icons pull-right">{this.getExtensionIcons()}</div> |
| </header> |
| {this.getDocContent()} |
| </div> |
| <div className="clearfix"></div> |
| </div> |
| ); |
| } |
| }); |
| |
| var LoadLines = React.createClass({ |
| render: function () { |
| return ( |
| <div className="loading-lines"> |
| <div id="line1"> </div> |
| <div id="line2"> </div> |
| <div id="line3"> </div> |
| <div id="line4"> </div> |
| </div> |
| ); |
| } |
| }); |
| |
| var ConfirmButton = React.createClass({ |
| propTypes: { |
| showIcon: React.PropTypes.bool |
| }, |
| |
| getDefaultProps: function () { |
| return { |
| showIcon: true |
| }; |
| }, |
| |
| getIcon: function () { |
| if (!this.props.showIcon) { |
| return null; |
| } |
| return ( |
| <i className="icon fonticon-ok-circled" /> |
| ); |
| }, |
| |
| render: function () { |
| return ( |
| <button onClick={this.props.onClick} type="submit" className="btn btn-success save" id={this.props.id}> |
| {this.getIcon()} |
| {this.props.text} |
| </button> |
| ); |
| } |
| }); |
| |
| var MenuDropDown = React.createClass({ |
| |
| getDefaultProps: function () { |
| return { |
| icon: 'fonticon-plus-circled' |
| }; |
| }, |
| |
| createSectionLinks: function (links) { |
| if (!links) { return null; } |
| |
| return links.map(function (link, key) { |
| return this.createEntry(link, key); |
| }.bind(this)); |
| }, |
| |
| createEntry: function (link, key) { |
| return ( |
| <li key={key}> |
| <a className={link.icon ? 'icon ' + link.icon : ''} |
| data-bypass={link.external ? 'true' : ''} |
| href={link.url} |
| onClick={link.onClick} |
| target={link.external ? '_blank' : ''}> |
| {link.title} |
| </a> |
| </li> |
| ); |
| }, |
| |
| createSectionTitle: function (title) { |
| if (!title) { |
| return null; |
| } |
| |
| return ( |
| <li className="header-label">{title}</li> |
| ); |
| }, |
| |
| createSection: function () { |
| return this.props.links.map(function (linkSection, key) { |
| if (linkSection.title && linkSection.links) { |
| return ([ |
| this.createSectionTitle(linkSection.title), |
| this.createSectionLinks(linkSection.links) |
| ]); |
| } |
| |
| return this.createEntry(linkSection, 'el' + key); |
| |
| }.bind(this)); |
| }, |
| |
| render: function () { |
| return ( |
| <div className="dropdown"> |
| <a className={"dropdown-toggle icon " + this.props.icon} |
| data-toggle="dropdown" |
| href="#" |
| data-bypass="true"></a> |
| <ul className="dropdown-menu arrow" role="menu" aria-labelledby="dLabel"> |
| {this.createSection()} |
| </ul> |
| </div> |
| ); |
| } |
| }); |
| |
| var TrayContents = React.createClass({ |
| getChildren: function () { |
| var className = "tray show-tray " + this.props.className; |
| return ( |
| <div key={1} id={this.props.id} className={className}> |
| {this.props.children} |
| </div>); |
| }, |
| |
| render: function () { |
| return ( |
| <ReactCSSTransitionGroup transitionName="tray" transitionAppear={true} component="div" transitionAppearTimeout={500} |
| transitionEnterTimeout={500} transitionLeaveTimeout={300}> |
| {this.getChildren()} |
| </ReactCSSTransitionGroup> |
| ); |
| } |
| }); |
| |
| |
| function connectToStores (Component, stores, getStateFromStores) { |
| |
| var WrappingElement = React.createClass({ |
| |
| componentDidMount: function () { |
| stores.forEach(function (store) { |
| store.on('change', this.onChange, this); |
| }.bind(this)); |
| }, |
| |
| componentWillUnmount: function () { |
| stores.forEach(function (store) { |
| store.off('change', this.onChange); |
| }.bind(this)); |
| }, |
| |
| getInitialState: function () { |
| return getStateFromStores(this.props); |
| }, |
| |
| onChange: function () { |
| if (!this.isMounted()) { |
| return; |
| } |
| |
| this.setState(getStateFromStores(this.props)); |
| }, |
| |
| handleStoresChanged: function () { |
| if (this.isMounted()) { |
| this.setState(getStateFromStores(this.props)); |
| } |
| }, |
| |
| render: function () { |
| return <Component {...this.state} {...this.props} />; |
| } |
| |
| }); |
| |
| return WrappingElement; |
| } |
| |
| var TrayWrapper = React.createClass({ |
| getDefaultProps: function () { |
| return { |
| className: '' |
| }; |
| }, |
| |
| renderChildren: function () { |
| return React.Children.map(this.props.children, function (child, key) { |
| |
| const props = {}; |
| Object.keys(this.props).filter((k) => { |
| return this.props.hasOwnProperty(k); |
| }).map((k) => { |
| return props[k] = this.props[k]; |
| }); |
| |
| return React.cloneElement(child, props); |
| }.bind(this)); |
| }, |
| |
| render: function () { |
| return ( |
| <div> |
| {this.renderChildren()} |
| </div> |
| ); |
| } |
| }); |
| |
| var APIBar = React.createClass({ |
| propTypes: { |
| buttonVisible: React.PropTypes.bool.isRequired, |
| contentVisible: React.PropTypes.bool.isRequired, |
| docURL: React.PropTypes.string, |
| endpoint: React.PropTypes.string |
| }, |
| |
| showCopiedMessage: function () { |
| FauxtonAPI.addNotification({ |
| msg: 'The API URL has been copied to the clipboard.', |
| type: 'success', |
| clear: true |
| }); |
| }, |
| |
| getDocIcon: function () { |
| if (!this.props.docURL) { |
| return null; |
| } |
| return ( |
| <a |
| className="help-link" |
| data-bypass="true" |
| href={this.props.docURL} |
| target="_blank" |
| > |
| <i className="icon icon-question-sign"></i> |
| </a> |
| ); |
| }, |
| |
| getTray: function () { |
| if (!this.props.contentVisible) { |
| return null; |
| } |
| |
| return ( |
| <TrayContents className="tray show-tray api-bar-tray"> |
| <div className="input-prepend input-append"> |
| <span className="add-on"> |
| API URL |
| {this.getDocIcon()} |
| </span> |
| |
| <FauxtonComponents.ClipboardWithTextField |
| onClipBoardClick={this.showCopiedMessage} |
| text="Copy URL" |
| textToCopy={this.props.endpoint} |
| showCopyIcon={false} |
| uniqueKey="clipboard-apiurl" /> |
| |
| <div className="add-on"> |
| <a |
| data-bypass="true" |
| href={this.props.endpoint} |
| target="_blank" |
| className="btn" |
| > |
| View JSON |
| </a> |
| </div> |
| </div> |
| </TrayContents> |
| ); |
| }, |
| |
| toggleTrayVisibility: function () { |
| Actions.toggleApiBarVisibility(!this.props.contentVisible); |
| }, |
| |
| componentDidMount: function () { |
| $('body').on('click.APIBar', function (e) { |
| if ($(e.target).closest('.api-bar-tray,.control-toggle-api-url').length === 0) { |
| Actions.toggleApiBarVisibility(false); |
| } |
| }.bind(this)); |
| }, |
| |
| componentWillUnmount: function () { |
| $('body').off('click.APIBar'); |
| }, |
| |
| render: function () { |
| if (!this.props.buttonVisible || !this.props.endpoint) { |
| return null; |
| } |
| |
| return ( |
| <div> |
| <ToggleHeaderButton |
| containerClasses="header-control-box control-toggle-api-url" |
| title="API URL" |
| fonticon="fonticon-link" |
| text="API" |
| toggleCallback={this.toggleTrayVisibility} /> |
| |
| {this.getTray()} |
| </div> |
| ); |
| } |
| }); |
| |
| var ApiBarController = React.createClass({ |
| |
| getWrap: function () { |
| return connectToStores(TrayWrapper, [componentStore], function () { |
| return { |
| buttonVisible: componentStore.getIsAPIBarButtonVisible(), |
| contentVisible: componentStore.getIsAPIBarVisible(), |
| endpoint: componentStore.getEndpoint(), |
| docURL: componentStore.getDocURL() |
| }; |
| }); |
| }, |
| |
| render: function () { |
| var TrayWrapper = this.getWrap(); |
| return ( |
| <TrayWrapper> |
| <APIBar buttonVisible={true} contentVisible={false} /> |
| </TrayWrapper> |
| ); |
| } |
| }); |
| |
| var DeleteDatabaseModal = React.createClass({ |
| |
| getInitialState: function () { |
| return { |
| inputValue: '', |
| disableSubmit: true |
| }; |
| }, |
| |
| propTypes: { |
| showHide: React.PropTypes.func.isRequired, |
| modalProps: React.PropTypes.object |
| }, |
| |
| close: function (e) { |
| if (e) { |
| e.preventDefault(); |
| } |
| |
| this.setState({ |
| inputValue: '', |
| disableSubmit: true |
| }); |
| |
| this.props.showHide({showModal: false}); |
| }, |
| |
| open: function () { |
| this.props.showHide({showModal: true}); |
| }, |
| |
| getDatabaseName: function () { |
| return this.props.modalProps.dbId.trim(); |
| }, |
| |
| onInputChange: function (e) { |
| var val = encodeURIComponent(e.target.value.trim()); |
| |
| this.setState({ |
| inputValue: val |
| }); |
| |
| this.setState({ |
| disableSubmit: val !== this.getDatabaseName() |
| }); |
| }, |
| |
| onDeleteClick: function (e) { |
| e.preventDefault(); |
| |
| Actions.deleteDatabase(this.getDatabaseName()); |
| }, |
| |
| onInputKeypress: function (e) { |
| if (e.keyCode === 13 && this.state.disableSubmit !== true) { |
| Actions.deleteDatabase(this.getDatabaseName()); |
| } |
| }, |
| |
| render: function () { |
| var isSystemDatabase = this.props.modalProps.isSystemDatabase; |
| var showDeleteModal = this.props.modalProps.showDeleteModal; |
| var dbId = this.props.modalProps.dbId; |
| |
| var warning = isSystemDatabase ? ( |
| <p style={{color: '#d14'}} className="warning"> |
| <b>You are about to delete a system database, be careful!</b> |
| </p> |
| ) : null; |
| |
| return ( |
| <Modal dialogClassName="delete-db-modal" show={showDeleteModal} onHide={this.close}> |
| <Modal.Header closeButton={true}> |
| <Modal.Title>Delete Database</Modal.Title> |
| </Modal.Header> |
| <Modal.Body> |
| {warning} |
| <p> |
| Warning: This action will permanently delete <code>{dbId}</code>. |
| To confirm the deletion of the database and all of the |
| database's documents, you must enter the database's name. |
| </p> |
| <input |
| type="text" |
| className="input-block-level" |
| onKeyUp={this.onInputKeypress} |
| onChange={this.onInputChange} |
| autoFocus={true} /> |
| </Modal.Body> |
| <Modal.Footer> |
| <button |
| disabled={this.state.disableSubmit} |
| onClick={this.onDeleteClick} |
| className="btn btn-danger delete"> |
| <i className="icon fonticon-cancel-circled" /> Delete |
| </button> |
| <a href="#" onClick={this.close} data-bypass="true" className="cancel-link">Cancel</a> |
| </Modal.Footer> |
| </Modal> |
| ); |
| } |
| }); |
| |
| return { |
| BadgeList: BadgeList, |
| Badge: Badge, |
| BulkActionComponent: BulkActionComponent, |
| ConfirmButton: ConfirmButton, |
| ToggleHeaderButton: ToggleHeaderButton, |
| StyledSelect: StyledSelect, |
| CodeEditorPanel: CodeEditorPanel, |
| CodeEditor: CodeEditor, |
| StringEditModal: StringEditModal, |
| ZenModeOverlay: ZenModeOverlay, |
| Beautify: Beautify, |
| PaddedBorderedBox: PaddedBorderedBox, |
| Document: Document, |
| LoadLines: LoadLines, |
| MenuDropDown: MenuDropDown, |
| TrayContents: TrayContents, |
| TrayWrapper: TrayWrapper, |
| connectToStores: connectToStores, |
| ApiBarController: ApiBarController, |
| renderMenuDropDown: function (el, opts) { |
| ReactDOM.render(<MenuDropDown icon="fonticon-vertical-ellipsis" links={opts.links} />, el); |
| }, |
| removeMenuDropDown: function (el) { |
| ReactDOM.unmountComponentAtNode(el); |
| }, |
| DeleteDatabaseModal: DeleteDatabaseModal |
| }; |
| |
| }); |