MozReview: Add appUtil, common css, and textEditorView forks for impending changes (
Bug 1246617). r?glob
MozReview-Commit-ID: EyUmAmFbAlN
new file mode 100644
--- /dev/null
+++ b/reviewboard/reviewboard/static/rb/css/common_mozreview.less
@@ -0,0 +1,587 @@
+@import (reference) "defs.less";
+
+
+.loading img {
+ margin-right: 4px;
+ vertical-align: text-bottom;
+}
+
+.box .star {
+ cursor: pointer;
+}
+
+
+/****************************************************************************
+ * Modal Boxes
+ ****************************************************************************/
+.modalbox {
+ margin: 10px;
+
+ .modalbox-contents {
+ margin: 10px;
+ position: relative; /* Makes this the offsetParent for calculations. */
+ }
+
+ .modalbox-buttons {
+ position: absolute;
+ margin: 10px;
+ text-align: right;
+ bottom: 0;
+ right: 0;
+
+ input {
+ margin-left: 10px;
+ }
+ }
+}
+
+/****************************************************************************
+ * Inline editor forms
+ ****************************************************************************/
+.editicon {
+ margin-left: @edit-icon-margin;
+
+ .rb-icon {
+ vertical-align: bottom;
+ }
+}
+
+.editable, .editicon {
+ line-height: 14px;
+}
+
+.inline-editor-form,
+.text-editor {
+ textarea {
+ border: 1px @textarea-border-color solid;
+ margin: 0;
+ outline: none;
+ padding: 10px;
+ .box-sizing(border-box);
+
+ /*
+ * This prevents extra spacing below a text area in different browsers.
+ * See http://stackoverflow.com/questions/7144843/extra-space-under-textarea-differs-along-browsers
+ */
+ vertical-align: top;
+ }
+}
+
+.inline-editor-form {
+ display: block;
+ margin: 0;
+ padding: 0;
+ white-space: nowrap;
+
+ .buttons input[type='button'] {
+ margin-left: 6px;
+ margin-right: 0;
+
+ &:first-child {
+ margin-left: 0;
+ }
+ }
+
+ input[type="text"] {
+ border: 1px #888a85 solid;
+ padding: 1px 2px;
+ }
+
+ input[type] + .buttons {
+ padding-left: 6px;
+ }
+
+ textarea {
+ margin-top: 5px;
+ }
+
+ textarea + .buttons,
+ .text-editor + .buttons {
+ margin-top: 6px;
+ white-space: normal;
+ }
+
+ .enable-markdown {
+ margin-left: 1em;
+
+ label {
+ color: black;
+ font-size: 8pt !important;
+ font-weight: normal;
+ }
+ }
+}
+
+.loading-indicator {
+ display: inline;
+}
+
+.text-editor {
+ margin: @textarea-editor-margin;
+
+ textarea, .CodeMirror {
+ margin: 0;
+ }
+}
+
+
+/****************************************************************************
+ * Forms
+ ****************************************************************************/
+.formdlg {
+ tr {
+ padding-top: 4px;
+ }
+
+ td.label {
+ white-space: nowrap;
+ }
+
+ .error {
+ color: #DD0000;
+ font-weight: bold;
+ margin-bottom: 10px;
+ padding: 4px 8px;
+ }
+
+ .errorlist {
+ display: block;
+ list-style: none;
+ margin: 0;
+ padding: 0;
+
+ li {
+ display: list-item;
+ padding: 2px 4px;
+ }
+ }
+
+ .spinner {
+ float: left;
+
+ img {
+ vertical-align: top;
+ }
+
+ h1 {
+ display: inline;
+ margin-left: 10px;
+ }
+ }
+}
+
+
+/****************************************************************************
+ * Account page
+ ****************************************************************************/
+.section {
+ margin-bottom: 20px;
+
+ .title {
+ background: #DDDDDD;
+ border: 1px black solid;
+ padding: 4px 8px;
+ }
+
+ .body {
+ margin-left: 20px;
+ padding: 10px;
+ }
+}
+
+#manual-updates {
+ padding-top: 1em;
+
+ .box-main {
+ padding: 1em;
+
+ h1, h2 {
+ margin-top: 2em;
+ }
+ }
+
+ p {
+ font-size: 9pt;
+ }
+}
+
+
+/****************************************************************************
+ * Login/Register pages
+ ****************************************************************************/
+@auth-section-margin: 2em;
+@auth-label-width: 12em;
+@auth-field-width: 16em;
+
+
+#auth_container {
+ font-size: 120%;
+ margin: 7em 0 0 0;
+ text-align: center;
+
+ input[type="text"],
+ input[type="password"],
+ input[type="email"] {
+ border: 1px #aaa solid;
+ font-size: inherit;
+ margin: 0;
+ padding: 0.5em;
+ width: @auth-field-width;
+ .border-radius(@box-border-radius);
+ .box-sizing(border-box);
+ }
+
+ .auth-button-container {
+ width: 16em;
+
+ input {
+ font-size: 120%;
+ margin: 0;
+ padding: 0.6em;
+ width: 100%;
+ .box-sizing(border-box);
+ }
+ }
+
+ .auth-form-row {
+ clear: both;
+ margin: 1.5em 0;
+ padding-left: @auth-label-width;
+
+ &.auth-field-row {
+ padding-left: 0;
+
+ label {
+ float: left;
+ font-weight: normal;
+ padding: 0.5em 1em 0.5em 0.5em;
+ text-align: right;
+ width: @auth-label-width;
+ .box-sizing(border-box);
+ }
+
+ .errorlist {
+ display: block;
+ margin: 0.5em 0 0 @auth-label-width;
+ width: @auth-field-width;
+
+ li {
+ font-weight: normal;
+ font-size: 9pt;
+ }
+ }
+ }
+ }
+
+ .auth-header {
+ margin: 0 0 1em 0;
+
+ h1 {
+ font-size: 120%;
+ margin: 1em 0;
+ padding: 0;
+ }
+
+ p {
+ color: #444;
+ margin: 1em 0;
+ }
+
+ .errorbox {
+ display: inline-block;
+ text-align: center;
+
+ .errorlist {
+ text-align: left;
+ }
+ }
+ }
+
+ .auth-section {
+ display: inline-block;
+ margin: 0 @auth-section-margin;
+ text-align: left;
+ vertical-align: top;
+
+ &.main-auth-section {
+ margin-left: (-@auth-label-width + @auth-section-margin);
+ }
+ }
+
+ .errorlist {
+ margin: 0;
+ padding: 0;
+
+ li {
+ display: block;
+ font-weight: normal;
+ margin: 0 0 1em 0;;
+ }
+ }
+
+ .on-mobile-medium-screen-720({
+ /* Waste less vertical space on mobile devices. */
+ margin-top: 3em;
+
+ .auth-form-row {
+ /* Change the labels to appear above the fields and not to the side. */
+ padding-left: 0;
+
+ &.auth-field-row {
+ label {
+ display: block;
+ float: none;
+ padding: 0.5em 0;
+ text-align: left;
+ width: auto;
+ }
+ }
+ }
+
+ .auth-section {
+ &.main-auth-section {
+ /* Reset the margin that was providing room for the side labels. */
+ margin-left: 0;
+ }
+ }
+ });
+}
+
+#auth_container #login_form {
+ .login-links {
+ margin-top: 2em;
+ text-align: center;
+
+ p {
+ width: @auth-field-width;
+
+ a {
+ color: blue;
+ text-decoration: none;
+ }
+ }
+ }
+}
+
+#auth_container #register_form {
+ .register-captcha-row {
+ padding-left: 0;
+
+ .register-captcha-container {
+ float: right;
+ }
+ }
+}
+
+
+/****************************************************************************
+ * Auto-complete widget
+ ****************************************************************************/
+
+.ui-autocomplete-results {
+ background: #ffffff;
+ border: 1px solid #808080;
+ overflow: hidden;
+ position: absolute;
+ width: 100%;
+ z-index: @z-index-menu;
+
+ .on-mobile-medium-screen-720({
+ &.search-results {
+ /*
+ * On mobile, set the autoresults list to take up the entire size of
+ * the #page-container, overriding anything set by the widget.
+ */
+ left: 0 !important;
+ top: -@page-container-padding !important;
+ width: 100% !important;
+ height: 100%;
+ border: 0;
+ overflow-y: auto;
+ }
+ });
+
+ ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ width: 100%;
+
+ .on-mobile-medium-screen-720({
+ max-height: none !important;
+ });
+
+ li {
+ cursor: pointer;
+ margin: 0;
+ padding: 3px 6px;
+ position: relative;
+ white-space: nowrap;
+
+ .on-mobile-medium-screen-720({
+ /*
+ * Give each item a border and more padding to help define the click
+ * area (and to fit in with typical search results on mobile devices.
+ */
+ border-top: 1px #EEE solid;
+ margin: 0 1em;
+ padding: 1.5em;
+ });
+
+ span {
+ margin-left: 6px;
+ position: absolute;
+ right: 6px;
+ }
+ }
+ }
+
+ .ui-autocomplete-over {
+ background: #71a5db;
+ color: #FFF;
+ }
+}
+
+.ui-autocomplete-footer {
+ background: #ECECEC;
+ border-top: 1px #C0C0C0 solid;
+ padding: 3px 6px;
+}
+
+
+/****************************************************************************
+ * User page hover
+ ****************************************************************************/
+
+#user_infobox {
+ .infobox-pic {
+ float: left;
+ width: 85px;
+ margin-right: 1em;
+ }
+
+ .infobox-text {
+ color: black;
+ float: left;
+ max-width: 200px;
+ word-wrap: break-word;
+ }
+
+ a {
+ text-decoration: none;
+
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+
+ h2 {
+ a {
+ color: inherit;
+ }
+ }
+
+ .logged-in {
+ margin-top: 2em;
+ }
+
+ .logged-in, .joined {
+ font-size: 0.8em;
+ }
+
+ #infobox-text h2 {
+ margin-top: 0;
+ padding-top: 0;
+ }
+}
+
+#submitter {
+ position: relative;
+}
+
+
+/****************************************************************************
+ * Bug hover
+ ****************************************************************************/
+
+#bug_infobox {
+ max-width: 50em;
+ max-height: 25em;
+ overflow: hidden;
+
+ .bug-infobox-text {
+ color: black;
+ float: left;
+ word-wrap: break-word;
+
+ h2 {
+ margin-top: 0;
+ padding-top: 0;
+ }
+ }
+}
+
+#user_infobox, #bug_infobox {
+ background: #F9F9F9;
+ border: 1px black solid;
+ display: block;
+ min-height: 12em;
+ padding: 1em;
+ position: absolute;
+ min-width: 40em;
+ z-index: @z-index-dialog;
+ .border-radius(10px);
+ .box-shadow(0px 0px 4px #000);
+
+ h2 {
+ font-size: 1.4em;
+ margin: 0 0 0.5em 0;
+ padding: 0;
+ }
+
+ p {
+ font-size: 1.2em;
+ margin: 0.5em 0;
+ padding: 0;
+ }
+}
+
+/****************************************************************************
+ * New Review Request
+ ****************************************************************************/
+#id_basedir
+#id_diff_path,
+#id_parent_diff_path {
+ width: 100%;
+}
+
+
+/****************************************************************************
+ * clearfix hacks
+ ****************************************************************************/
+
+/*
+ * clearfix hack. See http://www.webtoolkit.info/css-clearfix.html
+ */
+.clearfix {
+ display: inline-block;
+
+ &:after {
+ content: ".";
+ display: block;
+ clear: both;
+ visibility: hidden;
+ line-height: 0;
+ height: 0;
+ }
+}
+
+html[xmlns] .clearfix {
+ display: block;
+}
+
+* html .clearfix {
+ height: 1%;
+}
+
+// vim: set et ts=2 sw=2:
new file mode 100644
--- /dev/null
+++ b/reviewboard/reviewboard/static/rb/js/ui/views/textEditorView_mozreview.js
@@ -0,0 +1,589 @@
+(function() {
+
+
+var CodeMirrorWrapper,
+ TextAreaWrapper;
+
+
+/*
+ * Wraps CodeMirror, providing a standard interface for TextEditorView's usage.
+ */
+CodeMirrorWrapper = Backbone.View.extend({
+ /*
+ * Initializes CodeMirrorWrapper.
+ *
+ * This will set up CodeMirror based on the objects, add it to the parent,
+ * and begin listening to events.
+ */
+ initialize: function(options) {
+ var codeMirrorOptions = {
+ mode: {
+ name: 'gfm',
+ classOverrides: {
+ code: 'rb-markdown-code',
+ list1: 'rb-markdown-list1',
+ list2: 'rb-markdown-list2',
+ list3: 'rb-markdown-list3'
+ }
+ },
+ theme: 'rb default',
+ lineWrapping: true,
+ electricChars: false,
+ extraKeys: {
+ 'Home': 'goLineLeft',
+ 'End': 'goLineRight',
+ 'Enter': 'newlineAndIndentContinueMarkdownList',
+ 'Shift-Tab': false,
+ 'Tab': false
+ }
+ };
+
+ if (options.autoSize) {
+ codeMirrorOptions.viewportMargin = Infinity;
+ }
+
+ this._codeMirror = new CodeMirror(options.parentEl,
+ codeMirrorOptions);
+
+ this.setElement(this._codeMirror.getWrapperElement());
+
+ if (this.options.minHeight !== undefined) {
+ this.$el.css('min-height', this.options.minHeight);
+ }
+
+ this._codeMirror.on('viewportChange', _.bind(function() {
+ this.$el.triggerHandler('resize');
+ }, this));
+
+ this._codeMirror.on('change', _.bind(function() {
+ this.trigger('change');
+ }, this));
+ },
+
+ /*
+ * Returns whether or not the editor's contents have changed.
+ */
+ isDirty: function(initialValue) {
+ /*
+ * We cannot trust codeMirror's isClean() method.
+ *
+ * It is also possible for initialValue to be undefined, so we use an
+ * empty string in that case instead.
+ */
+ return (initialValue || '') !== this.getText();
+ },
+
+ /*
+ * Sets the text in the editor.
+ */
+ setText: function(text) {
+ this._codeMirror.setValue(text);
+ },
+
+ /*
+ * Returns the text in the editor.
+ */
+ getText: function() {
+ return this._codeMirror.getValue();
+ },
+
+ /*
+ * Returns the full client height of the content.
+ */
+ getClientHeight: function() {
+ return this._codeMirror.getScrollInfo().clientHeight;
+ },
+
+ /*
+ * Sets the size of the editor.
+ */
+ setSize: function(width, height) {
+ this._codeMirror.setSize(width, height);
+ this._codeMirror.refresh();
+ },
+
+ /*
+ * Focuses the editor.
+ */
+ focus: function() {
+ this._codeMirror.focus();
+ }
+});
+
+
+/*
+ * Wraps <textarea>, providing a standard interface for TextEditorView's usage.
+ */
+TextAreaWrapper = Backbone.View.extend({
+ tagName: 'textarea',
+
+ /*
+ * Initializes TextAreaWrapper.
+ *
+ * This will set up the element based on the provided options, begin
+ * listening for events, and add the element to the parent.
+ */
+ initialize: function(options) {
+ this.options = options;
+
+ if (options.autoSize) {
+ this.$el.autoSizeTextArea();
+ }
+
+ this.$el
+ .css('width', '100%')
+ .appendTo(options.parentEl)
+ .on('change keydown keyup keypress', _.bind(function() {
+ this.trigger('change');
+ }, this));
+
+ if (options.minHeight !== undefined) {
+ if (options.autoSize) {
+ this.$el.autoSizeTextArea('setMinHeight',
+ options.minHeight);
+ } else {
+ this.$el.css('min-height', this.options.minHeight);
+ }
+ }
+ },
+
+ /*
+ * Returns whether or not the editor's contents have changed.
+ */
+ isDirty: function(initialValue) {
+ var value = this.el.value || '';
+
+ return value.length !== initialValue.length ||
+ value !== initialValue;
+ },
+
+ /*
+ * Sets the text in the editor.
+ */
+ setText: function(text) {
+ this.el.value = text;
+
+ if (this.options.autoSize) {
+ this.$el.autoSizeTextArea('autoSize');
+ }
+ },
+
+ /*
+ * Returns the text in the editor.
+ */
+ getText: function() {
+ return this.el.value;
+ },
+
+ /*
+ * Returns the full client height of the content.
+ */
+ getClientHeight: function() {
+ return this.el.clientHeight;
+ },
+
+ /*
+ * Sets the size of the editor.
+ */
+ setSize: function(width, height) {
+ if (width !== null) {
+ this.$el.innerWidth(width);
+ }
+
+ if (height !== null) {
+ if (height === 'auto' && this.options.autoSize) {
+ this.$el.autoSizeTextArea('autoSize', true);
+ } else {
+ this.$el.innerHeight(height);
+ }
+ }
+ },
+
+ /*
+ * Focuses the editor.
+ */
+ focus: function() {
+ this.$el.focus();
+ }
+});
+
+
+/*
+ * Provides an editor for editing plain or Markdown text.
+ *
+ * The editor allows for switching between plain or Markdown text on-the-fly.
+ *
+ * When editing plain text, this uses a standard textarea widget.
+ *
+ * When editing Markdown, this makes use of CodeMirror. All Markdown content
+ * will be formatted as the user types, making it easier to notice when a
+ * stray _ or ` will cause Markdown-specific behavior.
+ */
+RB.TextEditorView = Backbone.View.extend({
+ className: 'text-editor',
+
+ defaultOptions: {
+ autoSize: true,
+ minHeight: 70
+ },
+
+ events: {
+ 'focus': 'focus'
+ },
+
+ /*
+ * Initializes the view with any provided options.
+ */
+ initialize: function(options) {
+ this._editor = null;
+ this._prevClientHeight = null;
+
+ this.options = _.defaults(options || {}, this.defaultOptions);
+ this.richText = !!this.options.richText;
+ this._value = this.options.text || '';
+ this._richTextDirty = false;
+
+ if (this.options.bindRichText) {
+ this.bindRichTextAttr(this.options.bindRichText.model,
+ this.options.bindRichText.attrName);
+ }
+
+ /*
+ * If the user is defaulting to rich text, we're going to want to
+ * show the rich text UI by default, even if any bound rich text
+ * flag is set to False.
+ *
+ * This requires cooperation with the template or API results
+ * that end up backing this TextEditor. The expectation is that
+ * those will be providing escaped data for any plain text, if
+ * the user's set to use rich text by default. If this expectation
+ * holds, the user will have a consistent experience for any new
+ * text fields.
+ */
+ if (RB.UserSession.instance.get('defaultUseRichText')) {
+ this.setRichText(true);
+ }
+ },
+
+ /*
+ * Renders the text editor.
+ *
+ * This will set the class name on the element, ensuring we have a
+ * standard set of styles, even if this editor is bound to an existing
+ * element.
+ */
+ render: function() {
+ this.$el.addClass(this.className);
+
+ return this;
+ },
+
+ /*
+ * Sets whether or not rich text (Markdown) is to be usd.
+ *
+ * This can dynamically change the text editor to work in plain text
+ * or Markdown.
+ */
+ setRichText: function(richText) {
+ if (richText === this.richText) {
+ return;
+ }
+
+ if (this._editor) {
+ this._hideEditor();
+ this.richText = richText;
+ this._showEditor();
+
+ this._richTextDirty = true;
+
+ this.$el.triggerHandler('resize');
+ } else {
+ this.richText = richText;
+ }
+
+ this.trigger('change:richText', richText);
+ this.trigger('change');
+ },
+
+ /*
+ * Binds a richText attribute on a model to the mode on this editor.
+ *
+ * This editor's richText setting will stay in sync with the attribute
+ * on the given mode.
+ */
+ bindRichTextAttr: function(model, attrName) {
+ this.setRichText(model.get(attrName));
+
+ this.listenTo(model, 'change:' + attrName, function(model, value) {
+ this.setRichText(value);
+ });
+ },
+
+ /*
+ * Binds an Enable Markdown checkbox to this text editor.
+ *
+ * The checkbox will initially be set to the value of the editor's
+ * richText property. Toggling the checkbox will then manipulate that
+ * property.
+ */
+ bindRichTextCheckbox: function($checkbox) {
+ $checkbox
+ .prop('checked', this.richText)
+ .on('change', _.bind(function() {
+ this.setRichText($checkbox.prop('checked'));
+ }, this));
+
+ this.on('change:richText', function() {
+ $checkbox.prop('checked', this.richText);
+ }, this);
+ },
+
+ /*
+ * Binds the visibility of an element to the richText property.
+ *
+ * If richText ist true, the element will be shown. Otherwise, it
+ * will be hidden.
+ */
+ bindRichTextVisibility: function($el) {
+ $el.setVisible(this.richText);
+
+ this.on('change:richText', function() {
+ $el.setVisible(this.richText);
+ }, this);
+ },
+
+ /*
+ * Returns whether or not the editor's contents have changed.
+ */
+ isDirty: function(initialValue) {
+ return this._editor !== null &&
+ (this._richTextDirty ||
+ this._editor.isDirty(initialValue || ''));
+ },
+
+ /*
+ * Sets the text in the editor.
+ */
+ setText: function(text) {
+ if (text !== this.getText()) {
+ if (this._editor) {
+ this._editor.setText(text);
+ } else {
+ this._value = text;
+ }
+ }
+ },
+
+ /*
+ * Returns the text in the editor.
+ */
+ getText: function() {
+ return this._editor ? this._editor.getText() : this._value;
+ },
+
+ /*
+ * Sets the size of the editor.
+ */
+ setSize: function(width, height) {
+ if (this._editor) {
+ this._editor.setSize(width, height);
+ }
+ },
+
+ /*
+ * Shows the editor.
+ */
+ show: function() {
+ this.$el.show();
+ this._showEditor();
+ },
+
+ /*
+ * Hides the editor.
+ */
+ hide: function() {
+ this._hideEditor();
+ this.$el.hide();
+ },
+
+ /*
+ * Focuses the editor.
+ */
+ focus: function() {
+ if (this._editor) {
+ this._editor.focus();
+ }
+ },
+
+ /*
+ * Shows the actual editor wrapper.
+ *
+ * Any stored text will be transferred to the editor, and the editor
+ * will take control over all operations.
+ */
+ _showEditor: function() {
+ var EditorCls;
+
+ if (this.richText) {
+ EditorCls = CodeMirrorWrapper;
+ } else {
+ EditorCls = TextAreaWrapper;
+ }
+
+ this._editor = new EditorCls({
+ parentEl: this.el,
+ autoSize: this.options.autoSize,
+ minHeight: this.options.minHeight
+ });
+
+ this._editor.setText(this._value);
+ this._value = '';
+ this._richTextDirty = false;
+ this._prevClientHeight = null;
+
+ this._editor.$el.on('resize', _.throttle(_.bind(function() {
+ this.$el.triggerHandler('resize');
+ }, this), 250));
+
+ this.listenTo(this._editor, 'change', _.throttle(_.bind(function() {
+ var clientHeight;
+
+ /*
+ * Make sure that the editor wasn't closed before the throttled
+ * handler was reached.
+ */
+ if (this._editor === null) {
+ return;
+ }
+
+ clientHeight = this._editor.getClientHeight();
+
+ if (clientHeight !== this._prevClientHeight) {
+ this._prevClientHeight = clientHeight;
+ this.$el.triggerHandler('resize');
+ }
+
+ this.trigger('change');
+ }, this), 500));
+
+ this.focus();
+ },
+
+ /*
+ * Hides the actual editor wrapper.
+ *
+ * The last value from the editor will be stored for later retrieval.
+ */
+ _hideEditor: function() {
+ if (this._editor) {
+ this._value = this._editor.getText();
+ this._richTextDirty = false;
+ this._editor.remove();
+ this._editor = null;
+
+ this.$el.empty();
+ }
+ }
+}, {
+ /*
+ * Returns options used to display a TextEditorView in an inlineEditor.
+ *
+ * This will return an options dictionary that can be used with an
+ * inlineEditor. The inlineEditor will make use of the TextEditorView
+ * instead of a textarea.
+ *
+ * This can take options for the TextEditorView to change the default
+ * behavior.
+ */
+ getInlineEditorOptions: function(options) {
+ var textEditor;
+
+ return {
+ matchHeight: false,
+ multiline: true,
+
+ createMultilineField: function(editor) {
+ var $editor = editor.element,
+ origRichText;
+
+ textEditor = new RB.TextEditorView(options);
+ textEditor.render();
+
+ $editor.one('beginEdit', function() {
+ var $buttons = $editor.inlineEditor('buttons'),
+ $markdownRef,
+ $checkbox,
+ $span;
+
+ $span = $('<span/>')
+ .addClass('enable-markdown');
+
+ $checkbox = $('<input/>')
+ .attr({
+ id: _.uniqueId('markdown_check'),
+ type: 'checkbox'
+ })
+ .appendTo($span);
+ textEditor.bindRichTextCheckbox($checkbox);
+
+ $span.append($('<label/>')
+ .attr('for', $checkbox[0].id)
+ .text(gettext('Enable Markdown')));
+
+ $buttons.append($span);
+
+ $markdownRef = $('<a/>')
+ .addClass('markdown-info')
+ .attr({
+ href: MANUAL_URL + 'users/markdown/',
+ target: '_blank'
+ })
+ .text(gettext('Markdown Reference'))
+ .setVisible(textEditor.richText)
+ .appendTo($buttons);
+ textEditor.bindRichTextVisibility($markdownRef);
+ });
+
+ $editor.on('beginEdit', function() {
+ textEditor._showEditor();
+ origRichText = textEditor.richText;
+ });
+
+ $editor.on('cancel', function() {
+ textEditor._hideEditor();
+ textEditor.setRichText(origRichText);
+ });
+
+ $editor.on('complete', function() {
+ textEditor._hideEditor();
+ });
+
+ textEditor.$el.data('text-editor', textEditor);
+
+ return textEditor.$el;
+ },
+
+ setFieldValue: function(editor, value) {
+ textEditor.setText(value || '');
+ },
+
+ getFieldValue: function() {
+ return textEditor.getText();
+ },
+
+ isFieldDirty: function(editor, initialValue) {
+ return textEditor.isDirty(initialValue);
+ }
+ };
+ },
+
+ /*
+ * Returns the TextEditorView for an inlineEditor element.
+ */
+ getFromInlineEditor: function($editor) {
+ return $editor.inlineEditor('field').data('text-editor');
+ }
+});
+
+
+})();
new file mode 100644
--- /dev/null
+++ b/reviewboard/reviewboard/static/rb/js/utils/apiUtils_mozreview.js
@@ -0,0 +1,245 @@
+/* Convenience wrapper for enabling and disabling the activity indicator.
+ *
+ * status determines if the indicator will be enabled (true) or disabled
+ * (false).
+ *
+ * The following field of options are inspected:
+ *
+ * noActivityIndicator - specify not to use the activity indicator
+ * type - determines if we are loading (GET) or saving
+ * (POST) information
+ */
+RB.setActivityIndicator = function(status, options) {
+ var $activityIndicator = $("#activity-indicator"),
+ $indicatorText = $activityIndicator.children('.indicator-text');
+
+ if (status) {
+ if (RB.ajaxOptions.enableIndicator && !options.noActivityIndicator) {
+ $indicatorText
+ .text((options.type || options.type === "GET")
+ ? gettext("Loading...") : gettext("Saving..."));
+
+ $activityIndicator
+ .removeClass("error")
+ .show();
+ }
+ } else if (RB.ajaxOptions.enableIndicator &&
+ !options.noActivityIndicator &&
+ !$activityIndicator.hasClass("error")) {
+ $activityIndicator
+ .delay(250)
+ .fadeOut("fast");
+ }
+};
+
+/*
+ * Convenience wrapper for Review Board API functions. This will handle
+ * any button disabling/enabling, write to the correct path prefix, form
+ * uploading, and displaying server errors.
+ *
+ * options has the following fields:
+ *
+ * buttons - An optional list of buttons to disable/enable.
+ * form - A form to upload, if any.
+ * type - The request type (defaults to "POST").
+ * prefix - The prefix to put on the API path (after SITE_ROOT, before
+ * "api")
+ * path - The relative path to the Review Board API tree.
+ * data - Data to send with the request.
+ * success - An optional success callback. The default one will reload
+ * the page.
+ * error - An optional error callback, called after the error banner
+ * is displayed.
+ * complete - An optional complete callback, called after the success or
+ * error callbacks.
+ *
+ * @param {object} options The options, listed above.
+ */
+RB.apiCall = function(options) {
+ var prefix = options.prefix || "",
+ url = options.url || (SITE_ROOT + prefix + "api" + options.path);
+
+ function doCall() {
+ var $activityIndicator = $("#activity-indicator"),
+ data;
+
+ if (options.buttons) {
+ options.buttons.attr("disabled", true);
+ }
+
+ RB.setActivityIndicator(true, options);
+
+ data = $.extend(true, {
+ url: url,
+ data: options.data,
+ dataType: options.dataType || "json",
+ error: function(xhr, textStatus, errorThrown) {
+ var rsp = null,
+ responseText;
+
+ try {
+ rsp = $.parseJSON(xhr.responseText);
+ } catch (e) {
+ }
+
+ if ((rsp && rsp.stat) || xhr.status === 204) {
+ if ($.isFunction(options.success)) {
+ options.success(rsp, xhr.status);
+ }
+
+ return;
+ }
+
+ responseText = xhr.responseText;
+ $activityIndicator
+ .addClass("error")
+ .text(gettext("A server error occurred."))
+ .append(
+ $("<a/>")
+ .text(gettext("Show Details"))
+ .attr("href", "#")
+ .click(function() {
+ showErrorPage(xhr, responseText);
+ })
+ )
+ .append(
+ $("<a/>")
+ .text(gettext("Dismiss"))
+ .attr("href", "#")
+ .click(function() {
+ $activityIndicator.fadeOut("fast");
+ return false;
+ })
+ );
+
+ if ($.isFunction(options.error)) {
+ options.error(xhr, textStatus, errorThrown);
+ }
+ }
+ }, options);
+
+ data.complete = function(xhr, status) {
+ if (options.buttons) {
+ options.buttons.attr("disabled", false);
+ }
+
+ RB.setActivityIndicator(false, options);
+
+ if ($.isFunction(options.complete)) {
+ options.complete(xhr, status);
+ }
+
+ $.funcQueue("rbapicall").next();
+ };
+
+ if (data.data === null || data.data === undefined ||
+ typeof data.data === 'object') {
+ data.data = $.extend({
+ api_format: 'json'
+ }, data.data || {});
+ }
+
+ if (options.form) {
+ options.form.ajaxSubmit(data);
+ } else {
+ $.ajax(data);
+ }
+ }
+
+ function showErrorPage(xhr, data) {
+ var iframe = $('<iframe/>')
+ .width('100%'),
+ requestData = '(none)',
+ doc;
+
+ if (options.data) {
+ requestData = $.param(options.data);
+ }
+
+ $('<div class="server-error-box"/>')
+ .appendTo("body")
+ .append('<p><b>' + gettext('Error Code:') + '</b> ' + xhr.status + '</p>')
+ .append('<p><b>' + gettext('Error Text:') + '</b> ' + xhr.statusText + '</p>')
+ .append('<p><b>' + gettext('Request URL:') + '</b> ' + url + '</p>')
+ .append('<p><b>' + gettext('Request Data:') + '</b> ' + requestData + '</p>')
+ .append('<p class="response-data"><b>' + gettext('Response Data:') + '</b></p>')
+ .append(gettext('<p>There may be useful error details below. The following error page may be useful to your system administrator or when <a href="https://www.reviewboard.org/bugs/new/">reporting a bug</a>. To save the page, right-click the error below and choose "Save Page As," if available, or "View Source" and save the result as a <tt>.html</tt> file.</p>'))
+ .append(gettext('<p><b>Warning:</b> Be sure to remove any sensitive material that may exist in the error page before reporting a bug!</p>'))
+ .append(iframe)
+ .on("resize", function() {
+ iframe.height($(this).height() - iframe.position().top);
+ })
+ .modalBox({
+ stretchX: true,
+ stretchY: true,
+ title: gettext("Server Error Details")
+ });
+
+ doc = iframe[0].contentDocument || iframe[0].contentWindow.document;
+ doc.open();
+ doc.write(data);
+ doc.close();
+ }
+
+ options.type = options.type || "POST";
+
+ /* We allow disabling the function queue for the sake of unit tests. */
+ if (RB.ajaxOptions.enableQueuing && options.type !== "GET") {
+ $.funcQueue("rbapicall").add(doCall);
+ $.funcQueue("rbapicall").start();
+ } else {
+ doCall();
+ }
+};
+
+/*
+ * Parses API error information from a response and stores it.
+ *
+ * The xhr object provided will be extended with two new attributes:
+ * 'errorText' and 'errorPayload'. These represent the response's error
+ * message and full error payload, respectively.
+ */
+RB.storeAPIError = function(xhr) {
+ var rsp = null,
+ text;
+
+ try {
+ rsp = $.parseJSON(xhr.responseText);
+ text = rsp.err.msg;
+ } catch (e) {
+ text = 'HTTP ' + xhr.status + ' ' + xhr.statusText;
+ }
+
+ xhr.errorText = text;
+ xhr.errorPayload = rsp;
+};
+
+RB.ajaxOptions = {
+ enableQueuing: true,
+ enableIndicator: true
+};
+
+/*
+ * Call RB.apiCall instead of $.ajax.
+ *
+ * We wrap instead of assign for now so that we can hook in/override
+ * RB.apiCall with unit tests.
+ */
+Backbone.ajax = function(options) {
+ return RB.apiCall(options);
+};
+
+
+if (!XMLHttpRequest.prototype.sendAsBinary) {
+ XMLHttpRequest.prototype.sendAsBinary = function(datastr) {
+ var data = new Uint8Array(
+ Array.prototype.map.call(datastr, function(x) {
+ return x.charCodeAt(0) & 0xFF;
+ }));
+
+ XMLHttpRequest.prototype.send.call(this, data.buffer);
+ };
+}
+
+
+// vim: set et:sw=4:
--- a/reviewboard/reviewboard/staticbundles.py
+++ b/reviewboard/reviewboard/staticbundles.py
@@ -101,17 +101,17 @@ PIPELINE_JS = dict({
'common': {
'source_filenames': (
'rb/js/utils/backboneUtils.js',
'rb/js/utils/compatUtils.js',
'rb/js/utils/consoleUtils.js',
'rb/js/utils/underscoreUtils.js',
'rb/js/common.js',
'rb/js/utils/apiErrors.js',
- 'rb/js/utils/apiUtils.js',
+ 'rb/js/utils/apiUtils_mozreview.js',
'rb/js/utils/linkifyUtils.js',
'rb/js/utils/mathUtils.js',
'rb/js/utils/keyBindingUtils.js',
'rb/js/collections/baseCollection.js',
'rb/js/collections/filteredCollection.js',
'rb/js/extensions/models/aliases.js',
'rb/js/extensions/models/commentDialogHookModel.js',
'rb/js/extensions/models/reviewDialogCommentHookModel.js',
@@ -145,17 +145,17 @@ PIPELINE_JS = dict({
'rb/js/resources/models/screenshotModel.js',
'rb/js/resources/models/screenshotCommentModel.js',
'rb/js/resources/models/screenshotCommentReplyModel.js',
'rb/js/resources/models/validateDiffModel.js',
'rb/js/resources/collections/resourceCollection.js',
'rb/js/resources/collections/repositoryBranchesCollection.js',
'rb/js/resources/collections/repositoryCommitsCollection.js',
'rb/js/ui/views/dialogView.js',
- 'rb/js/ui/views/textEditorView.js',
+ 'rb/js/ui/views/textEditorView_mozreview.js',
'rb/js/models/userSessionModel_mozreview.js',
'rb/js/views/headerView.js',
'rb/js/views/collectionView.js'
),
'output_filename': 'rb/js/base.min.js',
},
'account-page': {
'source_filenames': (
@@ -315,17 +315,17 @@ PIPELINE_CSS = dict({
'rb/css/pages/base.less',
'rb/css/pages/search.less',
'rb/css/ui/banners.less',
'rb/css/ui/boxes.less',
'rb/css/ui/buttons.less',
'rb/css/ui/datagrids.less',
'rb/css/ui/forms.less',
'rb/css/ui/sidebars.less',
- 'rb/css/common.less',
+ 'rb/css/common_mozreview.less',
),
'output_filename': 'rb/css/common.min.css',
'absolute_paths': False,
},
'js-tests': {
'source_filenames': (
'rb/css/pages/js-tests.less',
),