JavaScript >> Javascript 文檔 >  >> React

在 JavaScript 中使用內容可編輯元素 (React)

任何元素都可以通過添加 contenteditable 來編輯 屬性。此屬性在整個網絡中使用,例如在 Google 表格中。我不會告訴你使用或不使用 contenteditable 應用程序中的元素。如果您選擇使用 contenteditable ,您可能會發現這篇文章很有用。

我將分享一堆我在使用 contenteditable 時發現的東西 ,以便其他人可以在一個地方找到所有內容。

先決條件

如果您正在使用 contenteditable 進行任何類型的 JavaScript 工作,您可能會在本文中找到一些有用的東西 ,但我將在 React 中使用我的示例。你應該已經了解 JavaScript,了解 Node,使用 create-react-app 建立一個 React 項目 等。

  • React 入門 - 概述和演練 - 如果您從未使用過 React。

和以往一樣,我不關心 UI/設計,所以我將使用 Semantic UI React 元素來插入簡單的默認樣式。

目標

我將使用 ContentEditable 在 React 中創建一個簡單的 CRUD 表 零件。我將展示一些您可能會遇到的問題,以及我使用的解決方案。

以下是問題:

  • 粘貼
  • 空格和特殊字符
  • 換行
  • 突出顯示
  • 專注

然後是一些關於數字/貨幣和編輯現有行的內容。

  • 查看已完成的演示和源代碼

設置

這是起始代碼的 CodeSandbox 演示。

我要在 ce-app 中建立一個 React 項目 .

npx create-react-app ce-app && cd ce-app

添加react-contenteditablesemantic-ui-react 作為依賴項。 react-contenteditable 是一個非常好的組件,它可以使用 contenteditable 更能忍受。

yarn add react-contenteditable semantic-ui-react

為簡單起見,我將把所有內容都放在 index.js 中 .我只是加載所有依賴項,使 App 組件,在狀態中放入一些假數據,

index.js

import React, { Component } from 'react'
import ReactDOM from 'react-dom'
import ContentEditable from 'react-contenteditable'
import { Table, Button } from 'semantic-ui-react'
import './styles.css'

class App extends Component {
  initialState = {
    store: [
      { id: 1, item: 'silver', price: 15.41 },
      { id: 2, item: 'gold', price: 1284.3 },
      { id: 3, item: 'platinum', price: 834.9 },
    ],
    row: {
      item: '',
      price: '',
    },
  }

  state = this.initialState

  // Methods will go here
  render() {
    const {
      store,
      row: { item, price },
    } = this.state

    return (
      <div className="App">
        <h1>React Contenteditable</h1>
        {/* Table will go here */}
      </div>
    )
  }
}

ReactDOM.render(<App />, document.getElementById('root'))

該表將 Item、Price 和 Action 作為標題,並映射到每行的狀態。每個單元格都有一個 ContentEditable 組件,或刪除行或添加新行的操作。

<Table celled>
  <Table.Header>
    <Table.Row>
      <Table.HeaderCell>Item</Table.HeaderCell>
      <Table.HeaderCell>Price</Table.HeaderCell>
      <Table.HeaderCell>Action</Table.HeaderCell>
    </Table.Row>
  </Table.Header>
  <Table.Body>
    {store.map((row) => {
      return (
        <Table.Row key={row.id}>
          <Table.Cell>{row.item}</Table.Cell>
          <Table.Cell>{row.price}</Table.Cell>
          <Table.Cell className="narrow">
            <Button
              onClick={() => {
                this.deleteRow(row.id)
              }}
            >
              Delete
            </Button>
          </Table.Cell>
        </Table.Row>
      )
    })}
    <Table.Row>
      <Table.Cell className="narrow">
        <ContentEditable
          html={item}
          data-column="item"
          className="content-editable"
          onChange={this.handleContentEditable}
        />
      </Table.Cell>
      <Table.Cell className="narrow">
        <ContentEditable
          html={price}
          data-column="price"
          className="content-editable"
          onChange={this.handleContentEditable}
        />
      </Table.Cell>
      <Table.Cell className="narrow">
        <Button onClick={this.addRow}>Add</Button>
      </Table.Cell>
    </Table.Row>
  </Table.Body>
</Table>

我們從三種方法開始:一種是添加一行,它將用新行更新存儲,並清空現有行;另一個刪除現有行。

addRow = () => {
  this.setState(({ row, store }) => {
    return {
      store: [...store, { ...row, id: store.length + 1 }],
      row: this.initialState.row,
    }
  })
}

deleteRow = (id) => {
  this.setState(({ store }) => ({
    store: store.filter((item) => id !== item.id),
  }))
}

最後,我們有 handleContentEditable 組件,每次對 ContentEditable 進行更改時都會調用該組件 , 通過 onChange .為了在多個可能的列中使用一個函數,我添加了一個 data-column 屬性到組件,所以我得到每個 ContentEditable 的鍵(列)和值 ,並設置 row .

handleContentEditable = (event) => {
  const { row } = this.state
  const {
    currentTarget: {
      dataset: { column },
    },
    target: { value },
  } = event

  this.setState({ row: { ...row, [column]: value } })
}

還有一點 CSS 讓它看起來不錯。

.App {
  margin: 2rem auto;
  max-width: 800px;
  font-family: sans-serif;
}

.ui.table td {
  padding: 1rem;
}

.ui.table td.narrow {
  padding: 0;
}

.ui.button {
  margin: 0 0.5rem;
}

.content-editable {
  padding: 1rem;
}

.content-editable:hover {
  background: #f7f7f7;
}

.content-editable:focus {
  background: #fcf8e1;
  outline: 0;
}

同樣,此時如果您迷路了,您可以在此演示中看到已完成的設置。

因此,設置完成後,您就有了一個表格,您可以在其中使用 contenteditable 添加新行 ,而不是 inputtextarea ,因此可以完全控制元素的樣式。

問題1:粘貼

好的,現在你有了你的應用程序。勤勞的用戶認為,哦,我可以從 Google 表格或 Excel 中復制粘貼,而不是手動輸入所有內容!

讓我複制一下……

粘貼進去……

看起來不錯。讓我們提交那個壞男孩。

呃,什麼? contenteditable 元素保留文本的格式樣式。即使直接從文本編輯器粘貼也不會粘貼純文本。沒有什麼是安全的。

由於顯然我們不想在這裡提交 HTML,所以我們需要創建一個只粘貼文本而不是格式的函數。

pasteAsPlainText = (event) => {
  event.preventDefault()

  const text = event.clipboardData.getData('text/plain')
  document.execCommand('insertHTML', false, text)
}

我們可以把它放在onPaste ContentEditable .

<ContentEditable onPaste={this.pasteAsPlainText} />

問題 2:空格和特殊字符

你可以輸入一些有空格的東西,然後提交,結果沒問題。

很酷,所以空格不是 contenteditable 的問題 對吧?

讓我們看看當您的用戶從某處粘貼它並意外保留短語前後的空格時會發生什麼。

偉大的。 &nsbp; ,您在 1998 年用於格式化您的網站的不間斷空間會在開頭和結尾保留。不僅如此,還有小於、大於和&。

所以我只是對這些字符做了一點查找和替換。

const trimSpaces = (string) => {
  return string
    .replace(/&nbsp;/g, '')
    .replace(/&amp;/g, '&')
    .replace(/&gt;/g, '>')
    .replace(/&lt;/g, '<')
}

如果我將它添加到 addRow 方法,我可以在它們提交之前​​修復它們。

addRow = () => {
  const trimSpaces = (string) => {
    return string
      .replace(/&nbsp;/g, '')
      .replace(/&amp;/g, '&')
      .replace(/&gt;/g, '>')
      .replace(/&lt;/g, '<')
  }

  this.setState(({ store, row }) => {
    const trimmedRow = {
      ...row,
      item: trimSpaces(row.item),
      id: store.length + 1,
    }
    return {
      store: [...store, trimmedRow],
      row: this.initialState.row,
    }
  })
}

問題 3:換行符

假設您的用戶可能會嘗試按 Enter 鍵而不是 Tab 鍵以進入下一項,這並非不可能。

這將創建一個換行符。

contenteditable 將按字面意思理解 .

所以我們可以禁用它。 13 是輸入的鍵碼。

disableNewlines = (event) => {
  const keyCode = event.keyCode || event.which

  if (keyCode === 13) {
    event.returnValue = false
    if (event.preventDefault) event.preventDefault()
  }
}

這將在 onKeyPress 屬性。

<ContentEditable onKeyPress={this.disableNewlines} />

問題4:突出顯示

當我們通過一個 contenteditable 已經存在的元素,光標返回到 div 的開頭。這不是很有幫助。相反,我將創建一個函數,在通過選項卡或鼠標選擇時突出顯示整個元素。

highlightAll = () => {
  setTimeout(() => {
    document.execCommand('selectAll', false, null)
  }, 0)
}

這將在 onFocus 上進行 屬性。

<ContentEditable onFocus={this.highlightAll} />

問題 5:提交後關注

目前,在提交一行後,焦點丟失了,這使得在填寫此表時無法獲得良好的流程。理想情況下,它會在提交一行後關注新行中的第一項。

首先,製作一個 ref 低於狀態。

firstEditable = React.createRef()

addRow 的末尾 函數,關注firstEditable 當前div .


this.firstEditable.current.focus()

ContentEditable 方便地有一個 innerRef 我們可以為此使用的屬性。

<ContentEditable innerRef={this.firstEditable} />

現在提交一行後,我們已經專注於下一行了。

處理數字和貨幣

這並不完全特定於 contenteditable ,但由於我使用 price 作為值之一,這裡有一些處理貨幣和數字的函數。

您可以使用 <input type="number"> 只允許 HTML 中前端的數字,但我們必須為 ContentEditable 創建自己的函數 .對於字符串,我們必須防止 keyPress 上的換行符 ,但對於貨幣,我們只允許 . , , , 和 0-9 .

validateNumber = (event) => {
  const keyCode = event.keyCode || event.which
  const string = String.fromCharCode(keyCode)
  const regex = /[0-9,]|\./

  if (!regex.test(string)) {
    event.returnValue = false
    if (event.preventDefault) event.preventDefault()
  }
}

當然,這仍然會讓像 1,00,0.00.00 這樣格式不正確的數字 通過,但我們在這裡只驗證單個按鍵的輸入。

<ContentEditable onKeyPress={this.validateNumber} />

編輯現有行

最後,現在我們只能編輯最後一行——一旦添加了一行,改變它的唯一方法就是刪除它並創建一個新行。如果我們可以實時編輯每一行就好了,對吧?

我會做一個新的方法來更新。它與行類似,不同之處在於它不是更改新行的狀態,而是通過存儲映射並根據索引進行更新。我又添加了一個 data 屬性 - 行。

handleContentEditableUpdate = (event) => {
  const {
    currentTarget: {
      dataset: { row, column },
    },
    target: { value },
  } = event

  this.setState(({ store }) => {
    return {
      store: store.map((item) => {
        return item.id === parseInt(row, 10)
          ? { ...item, [column]: value }
          : item
      }),
    }
  })
}

而不是只顯示行中的值,它們都將是 ContentEditable .

{store.map((row, i) => {
  return (
    <Table.Row key={row.id}>
      <Table.Cell className="narrow">
        <ContentEditable
          html={row.item}
          data-column="item"
          data-row={row.id}
          className="content-editable"
          onKeyPress={this.disableNewlines}
          onPaste={this.pasteAsPlainText}
          onFocus={this.highlightAll}
          onChange={this.handleContentEditableUpdate}
        />
      </Table.Cell>
      <Table.Cell className="narrow">
        <ContentEditable
          html={row.price.toString()}
          data-column="price"
          data-row={row.id}
          className="content-editable"
          onKeyPress={this.validateNumber}
          onPaste={this.pasteAsPlainText}
          onFocus={this.highlightAll}
          onChange={this.handleContentEditableUpdate}
        />
      </Table.Cell>
      ...
  )
})}

最後,我要添加 disabled={!item || !price}Button 元素以防止空條目通過。我們完成了!

完整代碼

查看已完成的演示和源代碼

這就是一切,以防萬一有些事情沒有意義。單擊上面的演示以獲取 CodeSandbox 源和前端。

import React, { Component } from 'react'
import ReactDOM from 'react-dom'
import ContentEditable from 'react-contenteditable'
import { Table, Button } from 'semantic-ui-react'
import './styles.css'

class App extends Component {
  initialState = {
    store: [
      { id: 1, item: 'silver', price: 15.41 },
      { id: 2, item: 'gold', price: 1284.3 },
      { id: 3, item: 'platinum', price: 834.9 },
    ],
    row: {
      item: '',
      price: '',
    },
  }

  state = this.initialState
  firstEditable = React.createRef()

  addRow = () => {
    const { store, row } = this.state
    const trimSpaces = (string) => {
      return string
        .replace(/&nbsp;/g, '')
        .replace(/&amp;/g, '&')
        .replace(/&gt;/g, '>')
        .replace(/&lt;/g, '<')
    }
    const trimmedRow = {
      ...row,
      item: trimSpaces(row.item),
    }

    row.id = store.length + 1

    this.setState({
      store: [...store, trimmedRow],
      row: this.initialState.row,
    })

    this.firstEditable.current.focus()
  }

  deleteRow = (id) => {
    const { store } = this.state

    this.setState({
      store: store.filter((item) => id !== item.id),
    })
  }

  disableNewlines = (event) => {
    const keyCode = event.keyCode || event.which

    if (keyCode === 13) {
      event.returnValue = false
      if (event.preventDefault) event.preventDefault()
    }
  }

  validateNumber = (event) => {
    const keyCode = event.keyCode || event.which
    const string = String.fromCharCode(keyCode)
    const regex = /[0-9,]|\./

    if (!regex.test(string)) {
      event.returnValue = false
      if (event.preventDefault) event.preventDefault()
    }
  }

  pasteAsPlainText = (event) => {
    event.preventDefault()

    const text = event.clipboardData.getData('text/plain')
    document.execCommand('insertHTML', false, text)
  }

  highlightAll = () => {
    setTimeout(() => {
      document.execCommand('selectAll', false, null)
    }, 0)
  }

  handleContentEditable = (event) => {
    const { row } = this.state
    const {
      currentTarget: {
        dataset: { column },
      },
      target: { value },
    } = event

    this.setState({ row: { ...row, [column]: value } })
  }

  handleContentEditableUpdate = (event) => {
    const {
      currentTarget: {
        dataset: { row, column },
      },
      target: { value },
    } = event

    this.setState(({ store }) => {
      return {
        store: store.map((item) => {
          return item.id === parseInt(row, 10)
            ? { ...item, [column]: value }
            : item
        }),
      }
    })
  }

  render() {
    const {
      store,
      row: { item, price },
    } = this.state

    return (
      <div className="App">
        <h1>React Contenteditable</h1>

        <Table celled>
          <Table.Header>
            <Table.Row>
              <Table.HeaderCell>Item</Table.HeaderCell>
              <Table.HeaderCell>Price</Table.HeaderCell>
              <Table.HeaderCell>Action</Table.HeaderCell>
            </Table.Row>
          </Table.Header>
          <Table.Body>
            {store.map((row, i) => {
              return (
                <Table.Row key={row.id}>
                  <Table.Cell className="narrow">
                    <ContentEditable
                      html={row.item}
                      data-column="item"
                      data-row={i}
                      className="content-editable"
                      onKeyPress={this.disableNewlines}
                      onPaste={this.pasteAsPlainText}
                      onFocus={this.highlightAll}
                      onChange={this.handleContentEditableUpdate}
                    />
                  </Table.Cell>
                  <Table.Cell className="narrow">
                    <ContentEditable
                      html={row.price.toString()}
                      data-column="price"
                      data-row={i}
                      className="content-editable"
                      onKeyPress={this.validateNumber}
                      onPaste={this.pasteAsPlainText}
                      onFocus={this.highlightAll}
                      onChange={this.handleContentEditableUpdate}
                    />
                  </Table.Cell>
                  <Table.Cell className="narrow">
                    <Button
                      onClick={() => {
                        this.deleteRow(row.id)
                      }}
                    >
                      Delete
                    </Button>
                  </Table.Cell>
                </Table.Row>
              )
            })}
            <Table.Row>
              <Table.Cell className="narrow">
                <ContentEditable
                  html={item}
                  data-column="item"
                  className="content-editable"
                  innerRef={this.firstEditable}
                  onKeyPress={this.disableNewlines}
                  onPaste={this.pasteAsPlainText}
                  onFocus={this.highlightAll}
                  onChange={this.handleContentEditable}
                />
              </Table.Cell>
              <Table.Cell className="narrow">
                <ContentEditable
                  html={price}
                  data-column="price"
                  className="content-editable"
                  onKeyPress={this.validateNumber}
                  onPaste={this.pasteAsPlainText}
                  onFocus={this.highlightAll}
                  onChange={this.handleContentEditable}
                />
              </Table.Cell>
              <Table.Cell className="narrow">
                <Button disabled={!item || !price} onClick={this.addRow}>
                  Add
                </Button>
              </Table.Cell>
            </Table.Row>
          </Table.Body>
        </Table>
      </div>
    )
  }
}

ReactDOM.render(<App />, document.getElementById('root'))

結論

希望對你有所幫助!


Tutorial JavaScript 教程
  1. 為什麼你應該重新考慮使用 Date.now

  2. 交互式預算規劃師。加入開源!

  3. Promise 方法:.all()、.any()、.finally()、.race()

  4. Jquery UI Draggable:將助手對齊到鼠標位置

  5. 如何安裝反應?

  6. 如何使用快速檢查開始在 JavaScript 中進行基於屬性的測試

  7. 使用 Stellar.js 進行視差滾動的介紹

  1. 單擊鏈接時如何關閉菜單?

  2. 使用 GraphQL 和 Hasura 在 React 中構建 Instagram 克隆 - 第一部分

  3. DevTools 提示:日誌點

  4. 使用 NestJS、Fastify 和 TypeORM 創建 REST 應用程序

  5. 如何使用 VS Code 在 Docker 中使用 Typescript 調試 Apollo 服務器

  6. 2019 年最佳 RESTful API 框架

  7. 日常 javascript 的函數式編程:地圖的力量

  1. 在 Angular 中深入研究 RxJS

  2. 用於 React Native 的 Square 應用內支付 SDK

  3. BootstrapVue — 自定義工具提示

  4. 5 個不錯的 jQuery Web 開發插件