tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

TopSiteForm.jsx (10526B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
      3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs";
      6 import { A11yLinkButton } from "content-src/components/A11yLinkButton/A11yLinkButton";
      7 import React from "react";
      8 import { TOP_SITES_SOURCE } from "./TopSitesConstants";
      9 import { TopSiteFormInput } from "./TopSiteFormInput";
     10 import { TopSiteLink } from "./TopSite";
     11 
     12 export class TopSiteForm extends React.PureComponent {
     13  constructor(props) {
     14    super(props);
     15    const { site } = props;
     16    this.state = {
     17      label: site ? site.label || site.hostname : "",
     18      url: site ? site.url : "",
     19      validationError: false,
     20      customScreenshotUrl: site ? site.customScreenshotURL : "",
     21      showCustomScreenshotForm: site ? site.customScreenshotURL : false,
     22      hasURLChanged: false,
     23      hasTitleChanged: false,
     24    };
     25    this.onClearScreenshotInput = this.onClearScreenshotInput.bind(this);
     26    this.onLabelChange = this.onLabelChange.bind(this);
     27    this.onUrlChange = this.onUrlChange.bind(this);
     28    this.onCancelButtonClick = this.onCancelButtonClick.bind(this);
     29    this.onClearUrlClick = this.onClearUrlClick.bind(this);
     30    this.onDoneButtonClick = this.onDoneButtonClick.bind(this);
     31    this.onCustomScreenshotUrlChange =
     32      this.onCustomScreenshotUrlChange.bind(this);
     33    this.onPreviewButtonClick = this.onPreviewButtonClick.bind(this);
     34    this.onEnableScreenshotUrlForm = this.onEnableScreenshotUrlForm.bind(this);
     35    this.validateUrl = this.validateUrl.bind(this);
     36  }
     37 
     38  onLabelChange(event) {
     39    this.setState({
     40      label: event.target.value,
     41      hasTitleChanged: true,
     42    });
     43  }
     44 
     45  onUrlChange(event) {
     46    this.setState({
     47      url: event.target.value,
     48      validationError: false,
     49      hasURLChanged: true,
     50    });
     51  }
     52 
     53  onClearUrlClick() {
     54    this.setState({
     55      url: "",
     56      validationError: false,
     57    });
     58  }
     59 
     60  onEnableScreenshotUrlForm() {
     61    this.setState({ showCustomScreenshotForm: true });
     62  }
     63 
     64  _updateCustomScreenshotInput(customScreenshotUrl) {
     65    this.setState({
     66      customScreenshotUrl,
     67      validationError: false,
     68    });
     69    this.props.dispatch({ type: at.PREVIEW_REQUEST_CANCEL });
     70  }
     71 
     72  onCustomScreenshotUrlChange(event) {
     73    this._updateCustomScreenshotInput(event.target.value);
     74  }
     75 
     76  onClearScreenshotInput() {
     77    this._updateCustomScreenshotInput("");
     78  }
     79 
     80  onCancelButtonClick(ev) {
     81    ev.preventDefault();
     82    this.props.onClose();
     83  }
     84 
     85  onDoneButtonClick(ev) {
     86    ev.preventDefault();
     87 
     88    if (this.validateForm()) {
     89      const site = { url: this.cleanUrl(this.state.url) };
     90      const { index } = this.props;
     91      const isEdit = !!this.props.site;
     92 
     93      if (this.state.label !== "") {
     94        site.label = this.state.label;
     95      }
     96 
     97      if (this.state.customScreenshotUrl) {
     98        site.customScreenshotURL = this.cleanUrl(
     99          this.state.customScreenshotUrl
    100        );
    101      } else if (this.props.site && this.props.site.customScreenshotURL) {
    102        // Used to flag that previously cached screenshot should be removed
    103        site.customScreenshotURL = null;
    104      }
    105 
    106      this.props.dispatch(
    107        ac.AlsoToMain({
    108          type: at.TOP_SITES_PIN,
    109          data: { site, index },
    110        })
    111      );
    112 
    113      if (isEdit) {
    114        this.props.dispatch(
    115          ac.UserEvent({
    116            source: TOP_SITES_SOURCE,
    117            event: "TOP_SITES_EDIT",
    118            action_position: index,
    119            hasTitleChanged: this.state.hasTitleChanged,
    120            hasURLChanged: this.state.hasURLChanged,
    121          })
    122        );
    123      } else if (!isEdit) {
    124        this.props.dispatch(
    125          ac.UserEvent({
    126            source: TOP_SITES_SOURCE,
    127            event: "TOP_SITES_ADD",
    128            action_position: index,
    129          })
    130        );
    131      }
    132 
    133      this.props.onClose();
    134    }
    135  }
    136 
    137  onPreviewButtonClick(event) {
    138    event.preventDefault();
    139    if (this.validateForm()) {
    140      this.props.dispatch(
    141        ac.AlsoToMain({
    142          type: at.PREVIEW_REQUEST,
    143          data: { url: this.cleanUrl(this.state.customScreenshotUrl) },
    144        })
    145      );
    146      this.props.dispatch(
    147        ac.UserEvent({
    148          source: TOP_SITES_SOURCE,
    149          event: "PREVIEW_REQUEST",
    150        })
    151      );
    152    }
    153  }
    154 
    155  cleanUrl(url) {
    156    // If we are missing a protocol, prepend http://
    157    if (!url.startsWith("http:") && !url.startsWith("https:")) {
    158      return `http://${url}`;
    159    }
    160    return url;
    161  }
    162 
    163  _tryParseUrl(url) {
    164    try {
    165      return new URL(url);
    166    } catch (e) {
    167      return null;
    168    }
    169  }
    170 
    171  validateUrl(url) {
    172    const validProtocols = ["http:", "https:"];
    173    const urlObj =
    174      this._tryParseUrl(url) || this._tryParseUrl(this.cleanUrl(url));
    175 
    176    return urlObj && validProtocols.includes(urlObj.protocol);
    177  }
    178 
    179  validateCustomScreenshotUrl() {
    180    const { customScreenshotUrl } = this.state;
    181    return !customScreenshotUrl || this.validateUrl(customScreenshotUrl);
    182  }
    183 
    184  validateForm() {
    185    const validate =
    186      this.validateUrl(this.state.url) && this.validateCustomScreenshotUrl();
    187 
    188    if (!validate) {
    189      this.setState({ validationError: true });
    190    }
    191 
    192    return validate;
    193  }
    194 
    195  _renderCustomScreenshotInput() {
    196    const { customScreenshotUrl } = this.state;
    197    const requestFailed = this.props.previewResponse === "";
    198    const validationError =
    199      (this.state.validationError && !this.validateCustomScreenshotUrl()) ||
    200      requestFailed;
    201    // Set focus on error if the url field is valid or when the input is first rendered and is empty
    202    const shouldFocus =
    203      (validationError && this.validateUrl(this.state.url)) ||
    204      !customScreenshotUrl;
    205    const isLoading =
    206      this.props.previewResponse === null &&
    207      customScreenshotUrl &&
    208      this.props.previewUrl === this.cleanUrl(customScreenshotUrl);
    209 
    210    if (!this.state.showCustomScreenshotForm) {
    211      return (
    212        <A11yLinkButton
    213          onClick={this.onEnableScreenshotUrlForm}
    214          className="enable-custom-image-input"
    215          data-l10n-id="newtab-topsites-use-image-link"
    216        />
    217      );
    218    }
    219    return (
    220      <div className="custom-image-input-container">
    221        <TopSiteFormInput
    222          errorMessageId={
    223            requestFailed
    224              ? "newtab-topsites-image-validation"
    225              : "newtab-topsites-url-validation"
    226          }
    227          loading={isLoading}
    228          onChange={this.onCustomScreenshotUrlChange}
    229          onClear={this.onClearScreenshotInput}
    230          shouldFocus={shouldFocus}
    231          typeUrl={true}
    232          value={customScreenshotUrl}
    233          validationError={validationError}
    234          titleId="newtab-topsites-image-url-label"
    235          placeholderId="newtab-topsites-url-input"
    236        />
    237      </div>
    238    );
    239  }
    240 
    241  render() {
    242    const { customScreenshotUrl } = this.state;
    243    const requestFailed = this.props.previewResponse === "";
    244    // For UI purposes, editing without an existing link is "add"
    245    const showAsAdd = !this.props.site;
    246    const previous =
    247      (this.props.site && this.props.site.customScreenshotURL) || "";
    248    const changed =
    249      customScreenshotUrl && this.cleanUrl(customScreenshotUrl) !== previous;
    250    // Preview mode if changes were made to the custom screenshot URL and no preview was received yet
    251    // or the request failed
    252    const previewMode = changed && !this.props.previewResponse;
    253    const previewLink = Object.assign({}, this.props.site);
    254    if (this.props.previewResponse) {
    255      previewLink.screenshot = this.props.previewResponse;
    256      previewLink.customScreenshotURL = this.props.previewUrl;
    257    }
    258    // Handles the form submit so an enter press performs the correct action
    259    const onSubmit = previewMode
    260      ? this.onPreviewButtonClick
    261      : this.onDoneButtonClick;
    262 
    263    const addTopsitesHeaderL10nId = "newtab-topsites-add-shortcut-header";
    264    const editTopsitesHeaderL10nId = "newtab-topsites-edit-shortcut-header";
    265    return (
    266      <form className="topsite-form" onSubmit={onSubmit}>
    267        <div className="form-input-container">
    268          <h3
    269            className="section-title grey-title"
    270            data-l10n-id={
    271              showAsAdd ? addTopsitesHeaderL10nId : editTopsitesHeaderL10nId
    272            }
    273          />
    274          <div className="fields-and-preview">
    275            <div className="form-wrapper">
    276              <TopSiteFormInput
    277                onChange={this.onLabelChange}
    278                value={this.state.label}
    279                titleId="newtab-topsites-title-label"
    280                placeholderId="newtab-topsites-title-input"
    281                autoFocusOnOpen={true}
    282              />
    283              <TopSiteFormInput
    284                onChange={this.onUrlChange}
    285                shouldFocus={
    286                  this.state.validationError &&
    287                  !this.validateUrl(this.state.url)
    288                }
    289                value={this.state.url}
    290                onClear={this.onClearUrlClick}
    291                validationError={
    292                  this.state.validationError &&
    293                  !this.validateUrl(this.state.url)
    294                }
    295                titleId="newtab-topsites-url-label"
    296                typeUrl={true}
    297                placeholderId="newtab-topsites-url-input"
    298                errorMessageId="newtab-topsites-url-validation"
    299              />
    300              {this._renderCustomScreenshotInput()}
    301            </div>
    302            <TopSiteLink
    303              link={previewLink}
    304              defaultStyle={requestFailed}
    305              title={this.state.label}
    306            />
    307          </div>
    308        </div>
    309        <section className="actions">
    310          <button
    311            className="cancel"
    312            type="button"
    313            onClick={this.onCancelButtonClick}
    314            data-l10n-id="newtab-topsites-cancel-button"
    315          />
    316          {previewMode ? (
    317            <button
    318              className="done preview"
    319              type="submit"
    320              data-l10n-id="newtab-topsites-preview-button"
    321            />
    322          ) : (
    323            <button
    324              className="done"
    325              type="submit"
    326              data-l10n-id={
    327                showAsAdd
    328                  ? "newtab-topsites-add-button"
    329                  : "newtab-topsites-save-button"
    330              }
    331            />
    332          )}
    333        </section>
    334      </form>
    335    );
    336  }
    337 }
    338 
    339 TopSiteForm.defaultProps = {
    340  site: null,
    341  index: -1,
    342 };