FileMaker Web ビューアのテキストエリアの挙動
Claris FileMaker の Web ビューアでおかしな挙動を見つけ、その対策を考えたので備忘録として残します。
おかしな挙動
macOS においてテキストエリアで左右矢印キーを入力すると四角い文字が挿入される。また、⌘z や⌘⇧z でアンドゥやリドゥが効かない。
対策
JavaScript でキーダウンを検知して、通常の動きをキャンセルしたり、入力された履歴をとり、アンドゥ、リドゥを自力で実装する。
data:text/html,
<html>
<head>
<style>
div {
padding-bottom: 0.5em;
}
</style>
</head>
<body>
<div>
<textarea class="custom-textarea" rows="10" cols="30"></textarea>
<textarea class="custom-textarea" rows="10" cols="30"></textarea>
</div>
<script>
function createTextArea(textarea, submitCallback) {
const history = [""];
let historyIndex = 0;
let lastInputTime = Date.now();
let isComposing = false;
const updateHistory = () => {
const now = Date.now();
if (now - lastInputTime < 100 || isComposing) {
lastInputTime = now;
return;
}
if (historyIndex < history.length - 1) {
history.splice(historyIndex + 1);
}
history.push(textarea.value);
historyIndex++;
lastInputTime = now;
};
const setHistory = (text) => {
history.length = 0;
history.push(text);
historyIndex = 0;
};
textarea.addEventListener('input', (e) => {
if (!isComposing) {
updateHistory(textarea);
}
});
textarea.addEventListener('compositionstart', (e) => {
isComposing = true;
});
textarea.addEventListener('compositionend', (e) => {
isComposing = false;
updateHistory(textarea);
});
textarea.onkeydown = (e) => {
const isEnterKey = (e) => e.key === 'Enter' || e.key === 'Return';
const isMetaOrCtrlKey = (e) => e.metaKey || e.ctrlKey;
const isArrowKey = (e) => e.key === 'ArrowRight' || e.key === 'ArrowLeft';
const isAtInputBoundary = (e) => {
return (e.key === 'ArrowRight' && e.target.value.length === e.target.selectionStart) ||
(e.key === 'ArrowLeft' && e.target.selectionStart === 0);
};
if (isArrowKey(e) && isAtInputBoundary(e) && !e.shiftKey) {
e.preventDefault();
} else if (isEnterKey(e) && isMetaOrCtrlKey(e)) {
submitCallback();
} else if (isComposing) {
return;
} else if (isMetaOrCtrlKey(e) && e.key === 'z' && !e.shiftKey) {
e.preventDefault();
if (historyIndex > 0) {
historyIndex--;
e.target.value = history[historyIndex];
setTimeout(() => {
lastInputTime = Date.now();
}, 0);
}
} else if (isMetaOrCtrlKey(e) && e.key === 'z' && e.shiftKey) {
e.preventDefault();
if (historyIndex < history.length - 1) {
historyIndex++;
e.target.value = history[historyIndex];
setTimeout(() => {
lastInputTime = Date.now();
}, 0);
}
}
};
return {
submit: () => {
alert("投稿されたよ");
setHistory("");
},
};
}
const textAreaInstances = [];
document.querySelectorAll('.custom-textarea').forEach((textareaElement) => {
const instance = createTextArea(textareaElement, () => {
instance.submit();
});
textAreaInstances.push(instance);
});
</script>
</html>
ややアンドゥ、リドゥの箇所は調整必要かもしれないし、FileMaker の Web ビューア側で解決すれば、このような実装はしなくて済む。
サンプル
スペシャルサンクス
さらに最適化した例。
data:text/html,
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TextArea Undo Redo</title>
</head>
<body>
<textarea id="textareaId1" rows="10" cols="50"></textarea>
<button id="clearButton1">Clear Undo History for TextArea 1</button>
<br><br>
<textarea id="textareaId2" rows="10" cols="50"></textarea>
<button id="clearButton2">Clear Undo History for TextArea 2</button>
<script>
class TextAreaUndoRedo {
constructor(textareaId, defaultText = '') {
this.textArea = document.getElementById(textareaId);
this.undoStack = [defaultText !== '' ? defaultText : ''];
this.redoStack = [];
this.isComposing = false;
this.initEventListeners();
}
initEventListeners() {
this.textArea.addEventListener('keydown', (event) => {
const isCursorAtLeftEdge = this.textArea.selectionStart === 0;
const isCursorAtRightEdge = this.textArea.selectionStart === this.textArea.value.length;
const isTextSelected = this.textArea.selectionStart !== this.textArea.selectionEnd;
if (event.key === 'ArrowLeft' && isCursorAtLeftEdge && !event.shiftKey && !isTextSelected) {
event.preventDefault();
}
if (event.key === 'ArrowRight' && isCursorAtRightEdge && !event.shiftKey && !isTextSelected) {
event.preventDefault();
}
if ((event.metaKey || event.ctrlKey) && event.key === 'z') {
event.preventDefault();
if (!event.shiftKey && this.undoStack.length > 1) {
this.redoStack.push(this.undoStack.pop());
this.textArea.value = this.undoStack[this.undoStack.length - 1];
} else if (event.shiftKey && this.redoStack.length > 0) {
this.undoStack.push(this.redoStack.pop());
this.textArea.value = this.undoStack[this.undoStack.length - 1];
}
}
});
this.textArea.addEventListener('input', () => {
if (!this.isComposing) {
const lastValue = this.undoStack[this.undoStack.length - 1];
if (this.textArea.value !== lastValue) {
this.undoStack.push(this.textArea.value);
this.redoStack = [];
}
}
});
this.textArea.addEventListener('compositionstart', () => {
this.isComposing = true;
});
this.textArea.addEventListener('compositionend', () => {
this.isComposing = false;
const lastValue = this.undoStack[this.undoStack.length - 1];
if (this.textArea.value !== lastValue) {
this.undoStack.push(this.textArea.value);
this.redoStack = [];
}
});
}
clearUndoHistory() {
this.textArea.value = '';
this.undoStack = [''];
this.redoStack = [];
}
}
function clearTextAreaHistory(textareaUndoRedoInstance) {
textareaUndoRedoInstance.clearUndoHistory();
textareaUndoRedoInstance.undoStack.push('');
}
// 初期化
const textareaUndoRedo1 = new TextAreaUndoRedo('textareaId1', '');
const textareaUndoRedo2 = new TextAreaUndoRedo('textareaId2', '');
// クリアボタンを取得し、クリックイベントリスナーを追加
const clearButton1 = document.getElementById('clearButton1');
clearButton1.addEventListener('click', () => clearTextAreaHistory(textareaUndoRedo1));
const clearButton2 = document.getElementById('clearButton2');
clearButton2.addEventListener('click', () => clearTextAreaHistory(textareaUndoRedo2));
</script>
</body>
</html>
この記事が気に入ったらサポートをしてみませんか?