
【UI改善】【片手操作】ざっくりスマホで編集するためのテキストエディタを自作した話【iPhoneショートカット】【Scriptable】
先日ご紹介したこちらの記事の謎のテキストエディタ。
通勤中に使っていたのですが、やはり使いづらく(笑)
またもUIを大幅に改善することに・・・
【難点】
✅それぞれの機能は便利だと思うのですが、1モードにつき1機能しか画面に表示されないため、機能を組み合わせての操作ができない。
例:「文字サイズ切り替えて」見やすくしつつ「矢印ボタン」でカーソル移動させて「文字選択」してコピーして「貼り付ける」
(この1連の操作でモード切替4回も行うのはストレス(泣))
✅タップ切り替えモードが6種類あるため、目当てのモード出すまで時間がかかる。
【画面イメージ】


ボタン群が隠れて再表示ボタンのみに
【変更点】
✅切り替えモードのボタンは廃止して全ての機能ボタンを1画面に表示。
✅貼付ボタンを長押しするとボタン群が非表示になり、代わりに「再表示」用のボタンのみの表示に切り替え。
✅選択していない状態で左ボタンを押すと全選択してくれる機能を改良して、全選択した内容をクリップボードに格納する仕様とした(この操作が多かったため)
✅異常終了緩和のため、終了処理を強化(最後の通知が消えるまで待つと次回起動時に安定する想定)
今回のレシピ本体
(レシピとは別にScriptableというアプリとコードの設定が必要となります。設定は前回の記事をご参照ください)
【Scriptableのコード】
// ========================================
const FILE_NAME = "editor_memo9999996.txt"
if(args.queryParameters.exit === "1"){
Pasteboard.copyString("NO_SAVE")
await new Promise(r=>Timer.schedule(0.4,false,r))
Safari.open("shortcuts://run-shortcut?name=エディタ")
Script.complete()
return
}
const fm = FileManager.local()
const path = fm.joinPath(fm.documentsDirectory(), FILE_NAME)
let initialText = ""
if (fm.fileExists(path)) initialText = fm.readString(path)
initialText = initialText.replace(/\\/g,"\\\\").replace(/`/g,"\\`")
// ========================================
const html = `
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<style>
body { margin:0; font-family: monospace; }
#warning{
height:16px;
background:#fdd;
color:#900;
font-size:8px;
display:flex;
align-items:center;
padding-left:6px;
}
#editorArea{ height:calc(100vh - 16px); }
#scroll{ height:100%; overflow:auto; }
#editor{
width:100%;
min-height:100%;
font-size:10px;
padding:4px;
box-sizing:border-box;
}
#overlay{
position:fixed;
right:6px;
top:6px;
display:grid;
grid-template-columns:1fr 1fr;
gap:4px;
width:48vw;
max-width:220px;
z-index:1000;
}
#showBtn{
display:none;
grid-column:2;
width:100%;
}
.hiddenAll > *:not(#showBtn){
display:none !important;
}
.hiddenAll #showBtn{
display:block !important;
}
/* ===== ここまで ===== */
#overlay input,
#overlay button{
width:100%;
box-sizing:border-box;
font-size:10px;
}
:root{ --btnH:3.2vh; }
#overlay input{ height:var(--btnH); }
#overlay button{
height:calc(var(--btnH) * 1.1);
touch-action:manipulation;
-webkit-user-select:none;
}
#overlay button:focus{ outline:none; }
</style>
</head>
<body>
<div id="warning">
⚠ closeは使わないでください
</div>
<div id="editorArea">
<div id="scroll">
<textarea id="editor"></textarea>
</div>
</div>
<div id="overlay">
<!-- 再表示 -->
<button id="showBtn">再表示</button>
<!-- 既存UIそのまま -->
<input id="searchBox" placeholder="変更前">
<input id="replaceBox" placeholder="変更後">
<button id="exitBtn">終了</button>
<button id="pasteBtn">貼付</button>
<button id="loadBtn">呼出</button>
<button id="saveBtn">保存</button>
<button id="searchBtn">検索</button>
<button id="replaceBtn">置換</button>
<button id="fontMinusBtn">文字-</button>
<button id="fontPlusBtn">文字+</button>
<button id="selMinusBtn">選択-</button>
<button id="selPlusBtn">選択+</button>
<button id="leftBtn">←</button>
<button id="rightBtn">→</button>
</div>
<script>
let editor = document.getElementById("editor")
const overlay = document.getElementById("overlay")
let hiddenMode = false
let fontSize = 10
// ===== UI切替(フォーカス触らない)=====
function toggleUI(show){
hiddenMode = !show
if(hiddenMode){
overlay.classList.add("hiddenAll")
}else{
overlay.classList.remove("hiddenAll")
}
}
// 再表示
showBtn.onclick = (e)=>{
e.preventDefault()
e.stopPropagation()
toggleUI(true)
}
// 貼付長押し
let pasteTimer=null
pasteBtn.addEventListener("touchstart",(e)=>{
e.preventDefault()
e.stopPropagation()
pasteTimer=setTimeout(()=>{
toggleUI(false)
},400)
})
pasteBtn.addEventListener("touchend",(e)=>{
e.preventDefault()
e.stopPropagation()
if(pasteTimer){
clearTimeout(pasteTimer)
window.result="GET_CLIP"
}
})
// ===== 元機能 =====
function moveCursor(d){
let p = editor.selectionStart + d
p = Math.max(0, Math.min(editor.value.length, p))
editor.selectionStart = p
editor.selectionEnd = p
}
function findNext(){
const w = searchBox.value
if(!w) return
let i = editor.value.indexOf(w, editor.selectionEnd)
if(i === -1) i = editor.value.indexOf(w, 0)
if(i !== -1){
editor.selectionStart = i
editor.selectionEnd = i + w.length
}
}
function replaceAll(){
const w = searchBox.value
const r = replaceBox.value
if(!w) return
editor.value = editor.value.split(w).join(r)
}
function setHold(btn, fn){
let timer=null
let interval=null
let hold=false
btn.addEventListener("touchstart",(e)=>{
e.preventDefault()
e.stopPropagation()
hold=false
timer=setTimeout(()=>{
hold=true
fn()
interval=setInterval(fn,80)
},350)
})
btn.addEventListener("touchend",(e)=>{
e.preventDefault()
e.stopPropagation()
if(timer) clearTimeout(timer)
if(interval) clearInterval(interval)
if(!hold) fn()
})
}
// ボタン
exitBtn.onclick = ()=>window.result="EXIT"
loadBtn.onclick = ()=>window.result="LOAD"
saveBtn.onclick = ()=>{
const t = editor.value
window.result = JSON.stringify({type:"save", text:t})
}
searchBtn.onclick = findNext
replaceBtn.onclick = replaceAll
setHold(leftBtn, ()=>moveCursor(-1))
setHold(rightBtn, ()=>moveCursor(1))
setHold(fontMinusBtn, ()=>{
fontSize--
editor.style.fontSize = fontSize+"px"
})
setHold(fontPlusBtn, ()=>{
fontSize++
editor.style.fontSize = fontSize+"px"
})
setHold(selPlusBtn, ()=>{
editor.selectionEnd = Math.min(editor.value.length, editor.selectionEnd + 1)
})
setHold(selMinusBtn, ()=>{
if(editor.selectionStart === editor.selectionEnd){
if(editor.value.length < 20000){
editor.selectionStart = 0
editor.selectionEnd = editor.value.length
window.result = JSON.stringify({
type:"copy_all",
text: editor.value
})
}
}else{
editor.selectionEnd = Math.max(editor.selectionStart, editor.selectionEnd - 1)
}
})
function applyPasteText(t){
const s = editor.selectionStart
const e = editor.selectionEnd
editor.value = editor.value.slice(0,s)+t+editor.value.slice(e)
const p = s+t.length
editor.selectionStart = p
editor.selectionEnd = p
}
// 初期
setTimeout(()=>{
editor.value = String(window.initialText || "")
// ★追加:カーソル先頭固定
editor.selectionStart = 0
editor.selectionEnd = 0
},0)
</script>
</body>
</html>
`
// ========================================
const wv = new WebView()
await wv.loadHTML(html)
wv.present(true)
await wv.evaluateJavaScript("window.initialText="+JSON.stringify(initialText), false)
await wv.evaluateJavaScript("editor.value=window.initialText", false)
await wv.evaluateJavaScript("editor.selectionStart=0;editor.selectionEnd=0", false)
// ===== ループ =====
while(true){
await new Promise(r=>Timer.schedule(0.3,false,r))
let result=null
try{
result = await wv.evaluateJavaScript("window.result", false)
}catch(e){
break
}
if(!result) continue
await wv.evaluateJavaScript("window.result=null", false)
let p=null
try{ p = JSON.parse(result) }catch(e){}
if(p && p.type==="save"){
try{ fm.writeString(path, p.text) }catch(e){}
continue
}
if(p && p.type==="copy_all"){
Pasteboard.copyString(p.text || "")
continue
}
if(result==="GET_CLIP"){
const clip = Pasteboard.pasteString() || ""
await wv.evaluateJavaScript("applyPasteText("+JSON.stringify(clip)+")", false)
continue
}
if(result==="LOAD"){
const text = fm.fileExists(path) ? fm.readString(path) : ""
await wv.evaluateJavaScript("editor.value="+JSON.stringify(text), false)
continue
}
if(result==="EXIT"){
Pasteboard.copyString("NO_SAVE")
try{wv.dismiss()}catch(e){}
await new Promise(r=>Timer.schedule(0.5,false,r))
Safari.open("scriptable:///run?scriptName=edit&exit=1")
Script.complete()
return
}
}今回はこのアプリが必要となります。
設定方法は前回記事をご参照ください。
まとめ
一連の入力する操作や編集がだいぶスムーズになったような気がしますね。個人的には使ってて楽しくなってきました
(^^)
iPhoneショートカットアプリ関連