<commit_before>/**
 * Manages quotes.
 *
 * @author      <NAME>
 * @copyright  2001-2019 WoltLab GmbH
 * @license     GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 * @module      WoltLabSuite/Core/Ui/Redactor/Quote
 * @woltlabExcludeBundle tiny
 */

import * as Core from "../../Core";
import DomUtil from "../../Dom/Util";
import * as EventHandler from "../../Event/Handler";
import * as Language from "../../Language";
import * as StringUtil from "../../StringUtil";
import UiDialog from "../Dialog";
import { DialogCallbackSetup } from "../Dialog/Data";
import { RedactorEditor } from "./Editor";
import * as UiRedactorMetacode from "./Metacode";
import * as UiRedactorPseudoHeader from "./PseudoHeader";

interface QuoteData {
  author: string;
  content: string;
  isText: boolean;
  link: string;
}

let _headerHeight = 0;

class UiRedactorQuote {
  protected readonly _editor: RedactorEditor;
  protected readonly _elementId: string;
  protected readonly _knownElements = new WeakSet<HTMLElement>();
  protected _quote: HTMLElement | null = null;

  /**
   * Initializes the quote management.
   */
  constructor(editor: RedactorEditor, button: JQuery) {
    this._editor = editor;
    this._elementId = this._editor.$element[0].id;

    EventHandler.add("com.woltlab.wcf.redactor2", `observe_load_${this._elementId}`, () => this._observeLoad());

    this._editor.button.addCallback(button, this._click.bind(this));

    // bind listeners on init
    this._observeLoad();

    // quote manager
    EventHandler.add("com.woltlab.wcf.redactor2", `insertQuote_${this._elementId}`, (data) => this._insertQuote(data));
  }

  /**
   * Inserts a quote.
   */
  protected _insertQuote(data: QuoteData): void {
    if (this._editor.WoltLabSource.isActive()) {
      return;
    }

    Core.interactWithRedactor(() => {
      EventHandler.fire("com.woltlab.wcf.redactor2", "showEditor");

      const editor = this._editor.core.editor()[0];
      this._editor.selection.restore();

      this._editor.buffer.set();

      // caret must be within a `<p>`, if it is not: move it
      let block = this._editor.selection.block();
      if (block === false) {
        this._editor.focus.end();
        block = this._editor.selection.block() as HTMLElement;
      }

      while (block && block.parentElement !== editor) {
        block = block.parentElement!;
      }

      const quote = document.createElement("woltlab-quote");
      quote.dataset.author = data.author;
      quote.dataset.link = data.link;

      let content = data.content;
      if (data.isText) {
        content = StringUtil.escapeHTML(content);
        content = `<p>${content}</p>`;
        content = content.replace(/\n\n/g, "</p><p>");
        content = content.replace(/\n/g, "<br>");
      } else {
        content = UiRedactorMetacode.convertFromHtml(this._editor.$element[0].id, content);
      }

      // bypass the editor as `insert.html()` doesn't like us
      quote.innerHTML = content;

      const blockParent = block.parentElement!;
      blockParent.insertBefore(quote, block.nextSibling);

      if (block.nodeName === "P" && (block.innerHTML === "<br>" || block.innerHTML.replace(/\u200B/g, "") === "")) {
        blockParent.removeChild(block);
      }

      // avoid adjacent blocks that are not paragraphs
      let sibling = quote.previousElementSibling;
      if (sibling && sibling.nodeName !== "P") {
        sibling = document.createElement("p");
        sibling.textContent = "\u200B";
        quote.insertAdjacentElement("beforebegin", sibling);
      }

      this._editor.WoltLabCaret.paragraphAfterBlock(quote);

      this._editor.buffer.set();
    });
  }

  /**
   * Toggles the quote block on button click.
   */
  protected _click(): void {
    this._editor.button.toggle({}, "woltlab-quote", "func", "block.format");

    const quote = this._editor.selection.block();
    if (quote && quote.nodeName === "WOLTLAB-QUOTE") {
      this._setTitle(quote);

      quote.addEventListener("click", (ev) => this._edit(ev));

      // work-around for Safari
      this._editor.caret.end(quote);
    }
  }

  /**
   * Binds event listeners and sets quote title on both editor
   * initialization and when switching back from code view.
   */
  protected _observeLoad(): void {
    document.querySelectorAll("woltlab-quote").forEach((quote: HTMLElement) => {
      if (!this._knownElements.has(quote)) {
        quote.addEventListener("mousedown", (ev) => this._edit(ev));
        this._setTitle(quote);

        this._knownElements.add(quote);
      }
    });
  }

  /**
   * Opens the dialog overlay to edit the quote's properties.
   */
  protected _edit(event: MouseEvent): void {
    const quote = event.currentTarget as HTMLElement;

    if (_headerHeight === 0) {
      _headerHeight = UiRedactorPseudoHeader.getHeight(quote);
    }

    // check if the click hit the header
    const offset = DomUtil.offset(quote);
    if (event.pageY > offset.top && event.pageY < offset.top + _headerHeight) {
      event.preventDefault();

      this._editor.selection.save();
      this._quote = quote;

      UiDialog.open(this);
    }
  }

  /**
   * Saves the changes to the quote's properties.
   *
   * @protected
   */
  _dialogSubmit(): void {
    const id = `redactor-quote-${this._elementId}`;
    const urlInput = document.getElementById(`${id}-url`) as HTMLInputElement;

    let url = urlInput.value.replace(/\u200B/g, "").trim();
    if (url.length && !/^https?:\/\/[^/]+/.test(url)) {
      url = `https://${url}`
    }

    const quote = this._quote!;

    // set author
    const author = document.getElementById(id + "-author") as HTMLInputElement;
    quote.dataset.author = author.value;

    // set url
    quote.dataset.link = url;

    this._setTitle(quote);
    this._editor.caret.after(quote);

    UiDialog.close(this);
  }

  /**
   * Sets or updates the quote's header title.
   */
  protected _setTitle(quote: HTMLElement): void {
    const title = Language.get("wcf.editor.quote.title", {
      author: quote.dataset.author!,<commit_msg>Add missing semicolon in Quote.ts<commit_after>/**
 * Manages quotes.
 *
 * @author      <NAME>
 * @copyright  2001-2019 WoltLab GmbH
 * @license     GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 * @module      WoltLabSuite/Core/Ui/Redactor/Quote
 * @woltlabExcludeBundle tiny
 */

import * as Core from "../../Core";
import DomUtil from "../../Dom/Util";
import * as EventHandler from "../../Event/Handler";
import * as Language from "../../Language";
import * as StringUtil from "../../StringUtil";
import UiDialog from "../Dialog";
import { DialogCallbackSetup } from "../Dialog/Data";
import { RedactorEditor } from "./Editor";
import * as UiRedactorMetacode from "./Metacode";
import * as UiRedactorPseudoHeader from "./PseudoHeader";

interface QuoteData {
  author: string;
  content: string;
  isText: boolean;
  link: string;
}

let _headerHeight = 0;

class UiRedactorQuote {
  protected readonly _editor: RedactorEditor;
  protected readonly _elementId: string;
  protected readonly _knownElements = new WeakSet<HTMLElement>();
  protected _quote: HTMLElement | null = null;

  /**
   * Initializes the quote management.
   */
  constructor(editor: RedactorEditor, button: JQuery) {
    this._editor = editor;
    this._elementId = this._editor.$element[0].id;

    EventHandler.add("com.woltlab.wcf.redactor2", `observe_load_${this._elementId}`, () => this._observeLoad());

    this._editor.button.addCallback(button, this._click.bind(this));

    // bind listeners on init
    this._observeLoad();

    // quote manager
    EventHandler.add("com.woltlab.wcf.redactor2", `insertQuote_${this._elementId}`, (data) => this._insertQuote(data));
  }

  /**
   * Inserts a quote.
   */
  protected _insertQuote(data: QuoteData): void {
    if (this._editor.WoltLabSource.isActive()) {
      return;
    }

    Core.interactWithRedactor(() => {
      EventHandler.fire("com.woltlab.wcf.redactor2", "showEditor");

      const editor = this._editor.core.editor()[0];
      this._editor.selection.restore();

      this._editor.buffer.set();

      // caret must be within a `<p>`, if it is not: move it
      let block = this._editor.selection.block();
      if (block === false) {
        this._editor.focus.end();
        block = this._editor.selection.block() as HTMLElement;
      }

      while (block && block.parentElement !== editor) {
        block = block.parentElement!;
      }

      const quote = document.createElement("woltlab-quote");
      quote.dataset.author = data.author;
      quote.dataset.link = data.link;

      let content = data.content;
      if (data.isText) {
        content = StringUtil.escapeHTML(content);
        content = `<p>${content}</p>`;
        content = content.replace(/\n\n/g, "</p><p>");
        content = content.replace(/\n/g, "<br>");
      } else {
        content = UiRedactorMetacode.convertFromHtml(this._editor.$element[0].id, content);
      }

      // bypass the editor as `insert.html()` doesn't like us
      quote.innerHTML = content;

      const blockParent = block.parentElement!;
      blockParent.insertBefore(quote, block.nextSibling);

      if (block.nodeName === "P" && (block.innerHTML === "<br>" || block.innerHTML.replace(/\u200B/g, "") === "")) {
        blockParent.removeChild(block);
      }

      // avoid adjacent blocks that are not paragraphs
      let sibling = quote.previousElementSibling;
      if (sibling && sibling.nodeName !== "P") {
        sibling = document.createElement("p");
        sibling.textContent = "\u200B";
        quote.insertAdjacentElement("beforebegin", sibling);
      }

      this._editor.WoltLabCaret.paragraphAfterBlock(quote);

      this._editor.buffer.set();
    });
  }

  /**
   * Toggles the quote block on button click.
   */
  protected _click(): void {
    this._editor.button.toggle({}, "woltlab-quote", "func", "block.format");

    const quote = this._editor.selection.block();
    if (quote && quote.nodeName === "WOLTLAB-QUOTE") {
      this._setTitle(quote);

      quote.addEventListener("click", (ev) => this._edit(ev));

      // work-around for Safari
      this._editor.caret.end(quote);
    }
  }

  /**
   * Binds event listeners and sets quote title on both editor
   * initialization and when switching back from code view.
   */
  protected _observeLoad(): void {
    document.querySelectorAll("woltlab-quote").forEach((quote: HTMLElement) => {
      if (!this._knownElements.has(quote)) {
        quote.addEventListener("mousedown", (ev) => this._edit(ev));
        this._setTitle(quote);

        this._knownElements.add(quote);
      }
    });
  }

  /**
   * Opens the dialog overlay to edit the quote's properties.
   */
  protected _edit(event: MouseEvent): void {
    const quote = event.currentTarget as HTMLElement;

    if (_headerHeight === 0) {
      _headerHeight = UiRedactorPseudoHeader.getHeight(quote);
    }

    // check if the click hit the header
    const offset = DomUtil.offset(quote);
    if (event.pageY > offset.top && event.pageY < offset.top + _headerHeight) {
      event.preventDefault();

      this._editor.selection.save();
      this._quote = quote;

      UiDialog.open(this);
    }
  }

  /**
   * Saves the changes to the quote's properties.
   *
   * @protected
   */
  _dialogSubmit(): void {
    const id = `redactor-quote-${this._elementId}`;
    const urlInput = document.getElementById(`${id}-url`) as HTMLInputElement;

    let url = urlInput.value.replace(/\u200B/g, "").trim();
    if (url.length && !/^https?:\/\/[^/]+/.test(url)) {
      url = `https://${url}`;
    }

    const quote = this._quote!;

    // set author
    const author = document.getElementById(id + "-author") as HTMLInputElement;
    quote.dataset.author = author.value;

    // set url
    quote.dataset.link = url;

    this._setTitle(quote);
    this._editor.caret.after(quote);

    UiDialog.close(this);
  }

  /**
   * Sets or updates the quote's header title.
   */
  protected _setTitle(quote: HTMLElement): void {
    const title = Language.get("wcf.editor.quote.title", {
      author: quote.dataset.author!,
{'fraction_non_alphanumeric': 0.15042147475243473, 'fraction_numerical': 0.004091987887715852, 'mean_word_length': 2.901660280970626, 'pattern_counts': {'":': 0, '<': 25, '<?xml version=': 0, '>': 37, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '661779', 'n_tokens_mistral': 3835, 'n_tokens_neox': 3706, 'n_words': 823}