概念のようなテキストエディタを構築する方法

スラッシュコマンドは、Notionsテキストエディタの構成要素です。

間違いなく、Notion.soは、メモの整理に関して私のお気に入りのアプリの1つです。不要なUI要素ではなく、コンテンツに注意を向けるため、最小限のデザインのテキストエディターが大好きです。

それでも、それはあなたにより良い執筆体験につながるたくさんの強力な機能を提供します。

たとえば、スラッシュコマンドは、私の書き込みフローを実際に強化した機能の1つです。キーボードから離れることなく、コンテンツを追加してスタイルを設定できます。UIボタンをクリックして新しい見出しを追加する代わりに/h1、「Enter」を押して入力するだけで、そこに移動できます。

Notionのもう1つの優れた点は、完全にWebベースのアプリケーションであるということです。そのため、実際にどのように構築されているのか、特にテキストエディタに興味がありました。実は、思ったほど難しくないことに気づきました。

この記事では、Notionのようなテキストエディターがどのように機能し、React.jsを使用して自分で作成する方法について詳しく見ていきます。

これを行うことにより、いくつかの重要なフロントエンドスキルを発見して学習します。

  • 作業DOMとそのノード
  • 作業イベント・リスナー
  • 高度な状態管理
最終的なアプリケーション!

内容

  1. 理論:どのようにAの概念のようなテキストエディタの仕事ですか?
  2. 実践:どのように我々はそれを再構築することはできますか?
  3. さらなる発展のためのアイデア
  4. リソース

編集可能なブロック

Notionのようなテキストエディタのコアコンセプトは、私がブロックと呼びたいものです。キーボードの「Enter」を押すたびに、新しいブロックが作成されます。つまり、基本的に、各ページは1つまたは複数のブロックで構成されます。

ユーザーの観点からは、ブロックにはコンテンツが含まれ、ブロックタイプに関連付けられた特定のスタイルがあります。ブロックのタイプは、見出し、引用符、または段落です。それぞれに独自のスタイリングがあります。

技術的な観点から、ブロックはいわゆるcontenteditable要素です。ほぼすべてのHTML要素を編集可能な要素に変えることができます。contenteditable="true"属性を追加するだけです。これは、要素をユーザーが編集できるようにする必要があるかどうかを示します。その場合、ユーザーは、inputまたはtextarea要素であるかのように、HTMLドキュメント内のコンテンツを直接編集できます。

概念は例で明らかになります:

ユーザーが見出しのタイプで新しいブロックを追加するとします。これにより、次のHTML出力が生成されます。

<h1 contenteditable="true"> I am editable by the user </h1>

スラッシュコマンド

ユーザーが「Enter」を押してページに新しいブロックを追加できるようになったので、追加されたブロックのタイプをどのように判断できますか?

Notionでは、スラッシュコマンドを使用してこれを行うことができます。

スラッシュコマンドの概念はシンプルでありながら効果的です。/ブロック内を入力するたびに、カーソルのすぐ上に先行入力メニューがポップアップ表示されます。このメニューには、使用可能なすべてのブロックタイプが表示されます。書きながら、クエリでブロックタイプをフィルタリングします。正しいブロックタイプを見つけたら、「Enter」を押すだけで、ブロックは選択したものに変わります。

このアプリケーションでは、すべてのスラッシュコマンドは対応するHTML要素と同等になります。たとえば/h1/pコマンドがH1見出しに変わり、コマンドが段落に変わることを意味します。

技術的には、のような既存のブロックは、ユーザーが「Enter」を入力して押すと、<h1 contenteditable="true"> </h1>結果的にに<p contenteditable="true"> </p>なります/p

実践:どうすればそれを再構築できますか?

理論的にどのように機能するかがわかったので、それを実践してみましょう。まず、新しいReactプロジェクトを作成します。create-react-appまたはその他のツールを使用してこれを行うことができます。

1 —編集可能なページを作成します

セットアップが完了したら、最初のコンポーネントを作成します。 editablePage.js

// Imports
const initialBlock = { id: uid(), html: "", tag: "p" };
class EditablePage extends React.Component {
constructor(props) {
super(props);
this.state = { blocks: [initialBlock] };
}
render() {
return (
<div className="Page">
{this.state.blocks.map((block, key) => {
return (
<div key={key} id={block.id}>
Tag: {block.tag}, Content: {block.html}
</div>
);
})}
</div>
);
}
}
export default EditablePage;
view raw editablePage.js hosted with ❤ by GitHub

ページコンポーネントは、後で作成するすべてのブロックを格納およびレンダリングします。最初に、一番上で定義する最初の最初のブロックのみを提供します。編集可能なブロックコンポーネントを作成すると、このブロックは空の段落要素に変わります。今のところ、プレーンdivコンテナをレンダリングするだけです。

注:すべてのブロックには一意のIDが必要なuid()ため、新しいブロックの初期化に使用できるヘルパー関数を作成しました。同じ機能を使用したい場合:

const uid = () => {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
};
view raw uid.js hosted with ❤ by GitHub

2 —編集可能なブロックを作成する

次のステップとして、divコンテナの代わりにレンダリングできる編集可能なブロックコンポーネントを作成します。そのためには、と呼ばれる外部依存関係をインストールする必要がありreact-contenteditableます。

React-Contenteditableは、Reactで編集可能な要素を非常に簡単に操作できるようにするパッケージです。それは私たちにとって多くの複雑さを抽象化するので、適切な小道具をContentEditableコンポーネントに渡すだけで済みます。パッケージによって公開されたコンポーネントが残りを処理します。

パッケージをインストールするには、 npm i react-contenteditable

次に、editableBlock.js:という名前の新しいファイルを作成します。

// Imports
class EditableBlock extends React.Component {
constructor(props) {
super(props);
this.onChangeHandler = this.onChangeHandler.bind(this);
this.contentEditable = React.createRef();
this.state = {
html: "",
tag: "p",
};
}
componentDidMount() {
this.setState({ html: this.props.html, tag: this.props.tag });
}
componentDidUpdate(prevProps, prevState) {
const htmlChanged = prevState.html !== this.state.html;
const tagChanged = prevState.tag !== this.state.tag;
if (htmlChanged || tagChanged) {
this.props.updatePage({
id: this.props.id,
html: this.state.html,
tag: this.state.tag
});
}
}
onChangeHandler(e) {
this.setState({ html: e.target.value });
}
render() {
return (
<ContentEditable
className="Block"
innerRef={this.contentEditable}
html={this.state.html}
tagName={this.state.tag}
onChange={this.onChangeHandler}
/>
);
}
}
export default EditableBlock;
view raw editableBlock.js hosted with ❤ by GitHub

編集可能なブロックはContentEditablereact-contenteditableパッケージからインポートしたコンポーネントをレンダリングします。渡すことによって、htmlおよびtagNameコンポーネントへの小道具として、我々は、表示されるべきであるとそれがどのように表示すべきかを定義することができます。tagNameはHTML要素タイプを定義し、したがってスタイルを定義することを忘れないでください。

後でブロックタイプを変更する場合(たとえば、pからh1)、tagName小道具を更新するだけです。とても簡単ですよね?

また、ブロックコンポーネントに高度な状態管理を追加しました。コンポーネントがマウントされると、小道具を介して最初のHTMLコンテンツとタグを受け取り、それらを状態で保存します。今後、ブロックコンポーネントはドラフト状態を完全に所有します。htmlまたはtagNamepropに対するその後の変更はすべて無視されます。

htmlor tagNamestateプロパティに関連する更新があった場合、componentDidUpdateライフサイクルフックでもページコンポーネントの状態を更新します。したがって、ページの不要な再レンダリングを回避します。

私たちの持つeditableBlockコンポーネントのセットアップを、我々は最終的に私達のページのコンポーネントでそれを使用することができます。

// Imports
const initialBlock = { id: uid(), html: "", tag: "p" };
class EditablePage extends React.Component {
constructor(props) {
super(props);
this.updatePageHandler = this.updatePageHandler.bind(this);
this.addBlockHandler = this.addBlockHandler.bind(this);
this.deleteBlockHandler = this.deleteBlockHandler.bind(this);
this.state = { blocks: [initialBlock] };
}
updatePageHandler(updatedBlock) {
const blocks = this.state.blocks;
const index = blocks.map((b) => b.id).indexOf(updatedBlock.id);
const updatedBlocks = [...blocks];
updatedBlocks[index] = {
...updatedBlocks[index],
tag: updatedBlock.tag,
html: updatedBlock.html
};
this.setState({ blocks: updatedBlocks });
}
addBlockHandler(currentBlock) {
const newBlock = { id: uid(), html: "", tag: "p" };
const blocks = this.state.blocks;
const index = blocks.map((b) => b.id).indexOf(currentBlock.id);
const updatedBlocks = [...blocks];
updatedBlocks.splice(index + 1, 0, newBlock);
this.setState({ blocks: updatedBlocks }, () => {
currentBlock.ref.nextElementSibling.focus();
});
}
deleteBlockHandler(currentBlock) {
const previousBlock = currentBlock.ref.previousElementSibling;
if (previousBlock) {
const blocks = this.state.blocks;
const index = blocks.map((b) => b.id).indexOf(currentBlock.id);
const updatedBlocks = [...blocks];
updatedBlocks.splice(index, 1);
this.setState({ blocks: updatedBlocks }, () => {
setCaretToEnd(previousBlock);
previousBlock.focus();
});
}
}
render() {
return (
<div className="Page">
{this.state.blocks.map((block, key) => {
return (
<EditableBlock
key={key}
id={block.id}
tag={block.tag}
html={block.html}
updatePage={this.updatePageHandler}
addBlock={this.addBlockHandler}
deleteBlock={this.deleteBlockHandler}
/>
);
})}
</div>
);
}
}
export default EditablePage;
view raw editablePage.js hosted with ❤ by GitHub

フックEditableBlock内のコンポーネントを使用するrenderことに加えて、ページコンポーネントに新しいメソッドを追加しました。

  • updateBlockHandler我々はすでに私たちのブロック成分に使用することを同期してページとブロック状態を維持するの世話をします。
  • addBlockHandler新しいブロックを追加し、それにフォーカスを設定します。
  • deleteBlockHandlerブロックを削除し、前のブロックにフォーカスを設定します。

注:deleteBlockHandlerでは、カーソルをブロックコンテンツの最後に手動で設定する別のヘルパー関数を使用します。で行ったように要素にフォーカスするだけの場合は、要素にaddBlockHandlerフォーカスしますが、カーソルはブロックコンテンツの最初に設定されます。同じsetCaretToEndヘルパー関数を使用する場合:

const setCaretToEnd = (element) => {
const range = document.createRange();
const selection = window.getSelection();
range.selectNodeContents(element);
range.collapse(false);
selection.removeAllRanges();
selection.addRange(range);
element.focus();
};
view raw setCaretToEnd.js hosted with ❤ by GitHub

3 —KeyDownリスナーを実装する

これまでのところ、最初に追加したブロックにのみテキストコンテンツを追加できます。最初のイベントリスナーを使って、物事をもう少しインタラクティブにしましょう。これにより、必要に応じてブロックを追加および削除できます。

内部editableBlock.jsonKeyDownHandlerContentEditableコンポーネントに渡すメソッドを作成します。

// Imports
class EditableBlock extends React.Component {
constructor(props) {
super(props);
// ...
this.onKeyDownHandler = this.onKeyDownHandler.bind(this);
this.contentEditable = React.createRef();
this.state = {
htmlBackup: null,
html: "",
tag: "p",
previousKey: ""
};
}
// ...
onKeyDownHandler(e) {
if (e.key === "/") {
this.setState({ htmlBackup: this.state.html });
}
if (e.key === "Enter") {
if (this.state.previousKey !== "Shift") {
e.preventDefault();
this.props.addBlock({
id: this.props.id,
ref: this.contentEditable.current
});
}
}
if (e.key === "Backspace" && !this.state.html) {
e.preventDefault();
this.props.deleteBlock({
id: this.props.id,
ref: this.contentEditable.current
});
}
this.setState({ previousKey: e.key });
}
render() {
return (
<ContentEditable
className="Block"
innerRef={this.contentEditable}
html={this.state.html}
tagName={this.state.tag}
onChange={this.onChangeHandler}
onKeyDown={this.onKeyDownHandler}
/>
);
}
}
export default EditableBlock;
view raw editableBlock.js hosted with ❤ by GitHub

前述のように、このイベントリスナーは、主にブロックの追加と削除を担当します。しかし、onKeyDownHandler少しずつ分解してみましょう。

  • どのキーが押されたかに関係なく、キーを状態で保存します。previousKeyキーの組み合わせを検出できるようにする必要があります。
  • ユーザーがを押す/と、現在のHTMLコンテンツのコピーが状態で保存されます。これは、ユーザーがブロックタイプを選択するプロセスを終了した後、クリーンなHTMLバージョンを復元するためです。
  • ユーザーがを押すEnterと、デフォルトの動作(つまり、新しい行の追加)が防止されます。代わりaddBlockに、ページコンポーネントで以前に作成した方法を使用して、新しいブロックを作成します。
  • ユーザーは引き続き何らかの方法で新しい行を追加できるはずなので、previousKeystateプロパティを使用してShift+Enterキーの組み合わせを検出します。その場合、新しいブロックを追加せず、デフォルトの「Enter」動作を許可します。
  • 最後に、ユーザーがメソッドを押すBackspaceと、空のブロックが削除されますdeleteBlock
これまでのところ、ブロックを作成および削除できます。

4 —選択メニューを追加します

前述のように、ユーザーがa/を入力すると、選択メニューが表示されます。メニューには、使用可能なすべてのブロックタイプが一覧表示されます。次に、特定のブロックタイプをクリックするか、一致するスラッシュコマンドを入力して、特定のブロックタイプを選択できます。

このような選択メニューを実装するには、最初に別の依存関係を追加する必要があります:match-sorter。これは、一致するブロックタイプのクエリに役立つシンプルなパッケージです。

インストールするには、次のコマンドを実行します。 npm i match-sorter

これが完了したらselectMenu.js、次の内容で呼び出される新しいファイルを作成します。

// Imports
const MENU_HEIGHT = 150;
const allowedTags = [
{
id: "page-title",
tag: "h1",
label: "Page Title"
},
{
id: "heading",
tag: "h2",
label: "Heading"
},
{
id: "subheading",
tag: "h3",
label: "Subheading"
},
{
id: "paragraph",
tag: "p",
label: "Paragraph"
}
];
class SelectMenu extends React.Component {
constructor(props) {
super(props);
this.keyDownHandler = this.keyDownHandler.bind(this);
this.state = {
command: "",
items: allowedTags,
selectedItem: 0
};
}
componentDidMount() {
document.addEventListener("keydown", this.keyDownHandler);
}
componentDidUpdate(prevProps, prevState) {
const command = this.state.command;
if (prevState.command !== command) {
const items = matchSorter(allowedTags, command, { keys: ["tag"] });
this.setState({ items: items });
}
}
componentWillUnmount() {
document.removeEventListener("keydown", this.keyDownHandler);
}
keyDownHandler(e) {
const items = this.state.items;
const selected = this.state.selectedItem;
const command = this.state.command;
switch (e.key) {
case "Enter":
e.preventDefault();
this.props.onSelect(items[selected].tag);
break;
case "Backspace":
if (!command) this.props.close();
this.setState({ command: command.substring(0, command.length - 1) });
break;
case "ArrowUp":
e.preventDefault();
const prevSelected = selected === 0 ? items.length - 1 : selected - 1;
this.setState({ selectedItem: prevSelected });
break;
case "ArrowDown":
case "Tab":
e.preventDefault();
const nextSelected = selected === items.length - 1 ? 0 : selected + 1;
this.setState({ selectedItem: nextSelected });
break;
default:
this.setState({ command: this.state.command + e.key });
break;
}
}
render() {
const x = this.props.position.x;
const y = this.props.position.y - MENU_HEIGHT;
const positionAttributes = { top: y, left: x };
return (
<div className="SelectMenu" style={positionAttributes}>
<div className="Items">
{this.state.items.map((item, key) => {
const selectedItem = this.state.selectedItem;
const isSelected = this.state.items.indexOf(item) === selectedItem;
return (
<div
className={isSelected ? "Selected" : null}
key={key}
role="button"
tabIndex="0"
onClick={() => this.props.onSelect(item.tag)}
>
{item.label}
</div>
);
})}
</div>
</div>
);
}
}
export default SelectMenu;
view raw selectMenu.js hosted with ❤ by GitHub

最上部では、ユーザーが選択できるブロックタイプを定義します。各タイプにはタグとラベルがあります。タグは使用するHTML要素タイプを定義し、ラベルはメニュー内の表示名を定義します。

コンポーネントがマウントされると、renderフック内の画面上のメニューの位置が計算されます。そこでは、を使用しますthis.props.position。このオブジェクトのすぐ上にメニューを表示するため、このオブジェクトにはカーソルの現在の位置が含まれています。

さらに、フックにkeyDownイベントリスナーをアタッチしcomponentDidMountます。入力したコマンドを状態で保存し、ユーザーがキーボードからブロックタイプを選択できるようにします。

command状態プロパティが変更されるたびに、インポートされたmatchedSorter関数を実行して、一致するブロックタイプをフィルタリングします。したがって、選択メニューには、コマンドに一致するブロックタイプのみが表示されます。

最後に、ユーザーが「Enter」を押すか、エントリをクリックすると、onSelect小道具を介して受け取ったメソッドを実行します。

確かに、これは一度にたくさんのコードでした。しかし、それを段階的に実行すれば、うまくいけば理解できるはずです。

5 —ブロックタイプの選択を追加

次に、物事をまとめることができます。選択メニューコンポーネントが配置されているので、次の場所に追加できますeditableBlock.js

// Imports
class EditableBlock extends React.Component {
constructor(props) {
super(props);
// ...
this.onKeyUpHandler = this.onKeyUpHandler.bind(this);
this.openSelectMenuHandler = this.openSelectMenuHandler.bind(this);
this.closeSelectMenuHandler = this.closeSelectMenuHandler.bind(this);
this.tagSelectionHandler = this.tagSelectionHandler.bind(this);
this.contentEditable = React.createRef();
this.state = {
htmlBackup: null,
html: "",
tag: "p",
previousKey: "",
selectMenuIsOpen: false,
selectMenuPosition: {
x: null,
y: null
}
};
}
// ...
onKeyUpHandler(e) {
if (e.key === "/") {
this.openSelectMenuHandler();
}
}
openSelectMenuHandler() {
const { x, y } = getCaretCoordinates();
this.setState({
selectMenuIsOpen: true,
selectMenuPosition: { x, y }
});
document.addEventListener("click", this.closeSelectMenuHandler);
}
closeSelectMenuHandler() {
this.setState({
htmlBackup: null,
selectMenuIsOpen: false,
selectMenuPosition: { x: null, y: null }
});
document.removeEventListener("click", this.closeSelectMenuHandler);
}
tagSelectionHandler(tag) {
this.setState({ tag: tag, html: this.state.htmlBackup }, () => {
setCaretToEnd(this.contentEditable.current);
this.closeSelectMenuHandler();
});
}
render() {
return (
<>
{this.state.selectMenuIsOpen && (
<SelectMenu
position={this.state.selectMenuPosition}
onSelect={this.tagSelectionHandler}
close={this.closeSelectMenuHandler}
/>
)}
<ContentEditable
className="Block"
innerRef={this.contentEditable}
html={this.state.html}
tagName={this.state.tag}
onChange={this.onChangeHandler}
onKeyDown={this.onKeyDownHandler}
onKeyUp={this.onKeyUpHandler}
/>
</>
);
}
}
export default EditableBlock;
view raw editableBlock.js hosted with ❤ by GitHub

まず、SelectMenuコンポーネントをインポートして条件付きでレンダリングします。状態プロパティによってその可視性を制御できますselectMenuIsOpen。これをtrueに設定すると、ContentEditableコンポーネントに加えてメニューがレンダリングされます。

次に、いつどこに表示するかを定義するロジックを実装しました。

メニューの開始と終了を処理する2つのメソッドを定義しました。それを開くと、最初に現在設定されているカーソルの座標を取得します。これは、カーソルの真上にあるメニューを開きたいためです。最後に、この位置を状態に保存し、selectMenuIsOpentrueに設定して、クリックリスナーをアタッチします。

クリックリスナーは、メニュー自体にあるかメニューの外側にあるかに関係なく、画面がもう一度クリックされると、選択メニューを閉じる責任があります。両方のアクションで閉じる必要があります。

その場合、私たちのcloseSelectMenuHandler方法はかなり簡単です。以前に設定した状態プロパティをリセットし、クリックリスナーを再度切り離します。

開始ハンドラーと終了ハンドラーを実装したら、実際にメニューを開くタイミングも定義する必要があります。

ユーザーの観点からは、ユーザーがを入力したらメニューを開きます/。したがって、this.openSelectMenuHandler()以前に定義したonKeyDownHandlerメソッドに呼び出しを追加するのは直感的ですよね?そこにあるので、ユーザーが/キーを押したかどうかはすでに確認しています。

実際、そのために別のイベントリスナーを追加する必要があります。メニューは、ユーザーが/キーを離したときにのみ表示されます。そうしないと、メニューの配置が正しく機能しません。したがって、そのための新しいkeyUpイベントリスナーを追加します。

今、取るべき最後のステップは1つだけです。onSelect関数を小道具として選択メニューコンポーネントに渡すことを覚えていますか?選択プロセスを機能させるには、編集可能なブロックコンポーネントでそれに関連するメソッドを定義する必要があります。

嬉しいことに、この方法はかなり簡単です。では、tagSelectionHandler選択したブロックタイプをtag引数として受け取ります。これを使用すると、tagName状態プロパティを更新したり、HTMLバックアップ、つまりコマンドを入力せずにHTMLコンテンツを復元したりできます。

そのプロセスが終了したら、カーソルを編集可能なブロックに再度設定し、メニューを閉じます。

そして最後に、信じられないかもしれませんが…完了です🎉

注:カーソル座標を取得するために、再びヘルパー関数を使用しました。同じ機能を使用したい場合は、次のとおりです。

const getCaretCoordinates = () => {
let x, y;
const selection = window.getSelection();
if (selection.rangeCount !== 0) {
const range = selection.getRangeAt(0).cloneRange();
range.collapse(false);
const rect = range.getClientRects()[0];
if (rect) {
x = rect.left;
y = rect.top;
}
}
return { x, y };
};

さらなる発展のためのアイデア

Notionのテキストエディタのコア機能が再構築されたので、さらなる開発のための完璧な基盤ができました。アプリケーションをさらに改善するためにアプリケーションに追加できる機能はたくさんあります。

私は常に既存のアプリからインスピレーションを得て、自分でそれらの機能を構築できるかどうかを確認するのが好きです。概念と同じです。次に、2つの具体的な例を示します。

選択に基づくコンテキストメニュー

これまでのところ、ユーザーがを入力した場合にのみ選択メニューを表示します/。しかし、ユーザーが既存のブロックのタイプをすばやく編集したい場合はどうでしょうか。または彼/彼女はブロックを削除したいですか?

ユーザーがコンテンツを選択するとポップアップするコンテキストメニュー

ここでは、コンテキストメニューが便利です。ユーザーがブロックのコンテンツを選択するたびに、2つのオプションを含むメニューが表示されます。既存のブロックのタイプを別のタイプに変更するか、シングルクリックでブロック自体を削除します。

ドラッグアンドドロップでブロックを再配置

基盤となるブロック構造があるため、コンテンツをかなり簡単に再配置できます。たぶん、ユーザーは、この1つの段落をページの上部に配置する必要があることに気付いたでしょう。Notionでは、スムーズなドラッグアンドドロップ機能を使用してブロックを簡単に並べ替えることができます。そして、私たちもそれを行うことができます!

ブロックを並べ替えるドラッグアンドドロップ機能

リソース

確かに、この記事では多くのことを取り上げました…しかし、Notionクローンを構築するアイデアとプロセスは、特にReactの初心者にとって、本当にクールでエキサイティングだと思います。したがって、私はあなたが必要とするすべてのものをあなたに提供したかったのです。

最終申請

CodeSandboxに最終的なアプリケーションの実用的な例があります。また、この記事では取り上げなかったすべてのCSSスタイリングも含まれています。

高度なアプリケーション

さらに、より多くの機能(前のセクションで説明したような機能など)でアプリケーションを強化したい場合は、私のより高度なアプリケーションをご覧ください。それもインスピレーションの良い源かもしれません。

この高度なNotionCloneには、サーバー上のユーザーコンテンツを永続化するバックエンド部分も含まれています。Node / Expressの基本的なCRUD操作に触れたい場合に最適です!

ライブデモ: https://notion-clone.kmuenster.com/

Githubリポジトリ: https://github.com/konstantinmuenster/notion-clone

ブラウザでのカーソルの操作

お気づきのように、私はアプリケーションでいくつかのヘルパー関数を使用しました。これらの関数は主に、カーソル座標の取得など、ブラウザでカーソルを操作するのに役立ちます。

これは開発中に大変苦労したことだったので、最近記事を公開しました。このトピックをさらに深く掘り下げたい場合は、次のとおりです。

コンテンツ編集可能な要素内のキャレットを見つける方法

いつものように、読んでくれてありがとう!この記事全体で質問や問題が発生した場合は、コメントセクションでお知らせください。したがって、必要に応じて記事を更新および改善できます。フィードバックをいただければ幸いです。

You may like

提案された投稿

ブックマークレットとは何ですか?JavaScriptを使用してChromiumとFirefoxでブックマークレットを作成する方法

ブックマークレットとは何ですか?JavaScriptを使用してChromiumとFirefoxでブックマークレットを作成する方法

ブックマークレットは、Webページを開く代わりにJavaScriptを実行するブラウザのブックマークです。これらは、ブックマークアプレット、お気に入り、またはJavaScriptブックマークとも呼ばれます。

タプルの10の高度な機能

タプルの10の高度な機能

タプルとは異なり、タプルに関する知識は固定されていません。実際、それは拡張することができます!タプルについてすべてを知っているわけではないに違いありません!それらについて学ぶことはたくさんあります。

Interesting For You

関連記事

パフォーマンスが最適化されたA / Bテストソリューション

パフォーマンスが最適化されたA / Bテストソリューション

アジェンダ:はじめに:TL;しかし、私は読むことができます:A / Bテスト、CloudFront、Lamba @ edgeについて既に知っている場合は、AWS Lambda @edgeを使用したA / Bテストに直接進んでください。A / Bテストとは何ですか?A / Bテストは、2つの異なるバージョンのウェブサイトに対するユーザーのエンゲージメントを比較することに焦点を当てたUX調査方法です。

fp-ts(Typescript)からOptionとEitherを使用する

関数型プログラミングが好きなのは、間違いを犯したり、うさぎの穴を掘ったりすることから、何年にもわたって数回節約できたからです。同じ入力が与えられた場合、出力は常に同じであることを知っていることは安心です。

SyncfusionBlazorファイルアップロードコンポーネントで画像をプレビューする方法

SyncfusionBlazorファイルアップロードコンポーネントで画像をプレビューする方法

Syncfusion Blazor File Uploadは、1つ以上のファイル、画像、ドキュメント、オーディオ、ビデオ、およびその他のファイルをサーバーにアップロードするためのコンポーネントです。これは、HTML5アップロードコンポーネント(<input type =” file”>)の拡張バージョンであり、複数のファイル選択、プログレスバー、自動アップロード、ドラッグアンドドロップ、フォルダー(ディレクトリ)アップロード、ファイルなどの豊富な機能セットを備えています。検証など。

React開発者として私が持っている6つの後悔

早くやりたかったこと

React開発者として私が持っている6つの後悔

Reactは学ぶのに最適なツールです。それは私たちが私たち自身の方法で物事を行うことを可能にします。

MORE COOL STUFF

「パイオニアウーマン」リードラモンドのお気に入りのドーナツ

「パイオニアウーマン」リードラモンドのお気に入りのドーナツ

パイオニアウーマンリードラモンドは時々甘いおやつを好む。どのドーナツが彼女のお気に入りか調べてください。

「ロキ」エピソード2:トム・ヒドルストンとヘッドライターのマイケル・ウォルドロンが「レディ・ロキ」について語る

「ロキ」エピソード2:トム・ヒドルストンとヘッドライターのマイケル・ウォルドロンが「レディ・ロキ」について語る

トム・ヒドルストンとマイケル・ウォルドロンは、エピソード2の終わりにロキの女性版の公開について話します。

「RHOC」:ケリー・ドッドがブラウンウィン・ウィンダムを非難-ブラボーから斧を手に入れたことでバーク

「RHOC」:ケリー・ドッドがブラウンウィン・ウィンダムを非難-ブラボーから斧を手に入れたことでバーク

ケリー・ドッドは、彼女の元「RHOC」の共演者であるブラウンウィン・ウィンダム・バークを非難し、ブラボーのリアリティシリーズから解雇されたと非難しています。

「ハンドメイドの物語」:ニックとコマンダーのローレンスはどのようにして彼らの計画を回避しましたか?

「ハンドメイドの物語」:ニックとコマンダーのローレンスはどのようにして彼らの計画を回避しましたか?

ニックブレインとコマンダーローレンスは、「ハンドメイドの物語」のシーズン4フィナーレで6月を助けるために実際に問題を抱えていないかもしれません。

100年の洪水は、99年間二度と会えないという意味ではありません

100年の洪水は、99年間二度と会えないという意味ではありません

真実は、これらの大洪水の1つがヒットする可能性は毎年同じです:1パーセント。

実験室で育てられた肉がペットフードの世界をどのように変えることができるか

実験室で育てられた肉がペットフードの世界をどのように変えることができるか

持続可能で環境への影響を低減した、ラボで作成されたペットフードの新時代が到来しています。しかし、ペット、そして彼らの人間はそれを好きになるでしょうか?

完璧なCuppaJoeが欲しいですか?あなた自身のコーヒー豆を焙煎する

完璧なCuppaJoeが欲しいですか?あなた自身のコーヒー豆を焙煎する

その完璧な一杯のコーヒーを世界で探していましたか?たぶん、あなた自身のコーヒー豆を焙煎する芸術と科学を学ぶことは行く方法です。

聖書の神秘的なメルキゼデクは誰でしたか?

聖書の神秘的なメルキゼデクは誰でしたか?

彼は創世記にほんの一瞬登場しますが、それでも彼はイエス・キリストの先駆者と見なされてきました。彼は本当に何でしたか、そしてどのようにして彼はイエスと関係を持つようになりましたか?

賭け金が最も高いときにブルックス・ケプカが支配する

賭け金が最も高いときにブルックス・ケプカが支配する

もう一度、ブルックス・ケプカはメジャーのためにガスをオンにします。ブルックス・ケプカはゴルフが本当に得意で、最大のステージでのゴルフも得意です。

ダラスマーベリックスのクレイジータイム

ダラスマーベリックスのクレイジータイム

MavsのオーナーであるMarkCuban(l。)と元ヘッドコーチのRickCarlisleダラスでのDonnieNelson-RickCarlisleの時代は終わりました。

さて、これらのプレーオフは先週、いくつかの予想外のターンをしました

さて、これらのプレーオフは先週、いくつかの予想外のターンをしました

ルディ・ゴベアとジャズはクリッパーズにノックアウトされることから1ゲーム離れています。それが来るのを見なかった。

ペリカンはシオンによって正しくしなければなりません

ペリカンはシオンによって正しくしなければなりません

ペリカンが賢い場合、彼らはザイオンをオフシーズンの意思決定に関与させるでしょう。まだ2年しか経っていないため、ザイオンウィリアムソンは来年の夏まで延長資格がありませんが、ルイジアナバスケットボールの歴史の中で最も重要なシーズンの1つをすでに楽しみにしています。

TikTokのインフルエンサーがカントリーミュージックを楽しんだ-だからジョージ・バージは彼らをスターにするかもしれない曲を書いた

TikTokのインフルエンサーがカントリーミュージックを楽しんだ-だからジョージ・バージは彼らをスターにするかもしれない曲を書いた

「1分、私はもうライブ音楽を演奏するつもりかどうか疑問に思っています、そしてそれからいくつかのTikTokビデオが行き、すべてを完全に検証します」とジョージ・バージはPEOPLEに話します

ディスカバリーの次のセレンゲティIIで野生動物が君臨する:劇的な初見を得る

ディスカバリーの次のセレンゲティIIで野生動物が君臨する:劇的な初見を得る

ディスカバリードキュメンタリーシリーズのセレンゲティIIは、タンザニアの野生動物の生活を追っています。そして、たくさんのドラマを約束します。ここでスニークピークを取得します

ピーウィーのプレイハウスでの役割で知られる俳優ジョン・パラゴン、66歳で死去

ピーウィーのプレイハウスでの役割で知られる俳優ジョン・パラゴン、66歳で死去

ジョン・パラゴンは4月に亡くなりましたが、彼の死因は現時点では明らかではありません。

44年後、ルイジアナ州の男性がフライドチキンレストランで妻の殺人で逮捕されました

44年後、ルイジアナ州の男性がフライドチキンレストランで妻の殺人で逮捕されました

ダイアン・レデット・ベガス(32歳)は1977年に背中に1発の銃創で亡くなりました

Languages