2016年10月28日金曜日

contentEditableでリッチテキストエディタ

HTML5では全ての要素でcontentEditableプロパティが使えます。
それを利用して「リッチテキスト」を作成するプログラムを作りました。
JavaScriptが扱える人なら容易に機能を拡張することができます。

何気なく思いついて作ってみたけど、なかなかの拡張性で実用性が高そう。



画面はこんな感じです。



「html」「編集不可」「clickハンドラ設置」などのボタンの下のエリアはiframeです。
起動直後はiframe内部は自由に編集できる状態になっています。
画像をドラッグ移動させたい時など必要に応じてcontentEditableのtrue/falseを切り替えられます。

「html」ボタンを押すとメモ帳が起動して、編集内容に対応するHTMLソースが表示されます。



上記ソースを「拡張子:htm」などにして保存して



そのファイルを開くと、編集中の表示そのままのHTMLが表示されます。



ちなみに先ほどのメモ帳の画面で、中身を編集して上書き保存し、



メモ帳を閉じると編集プログラム側の表示に編集内容が反映されます。



以下、ソース
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>contentEditable</title>
<style>
html,body{width:100%;height:100%;}
.w-max{width:100%;}
.max{width:100%;height:100%;}
.of-auto{overflow:auto;}
.b1{border:solid 1px #000;}
.no_mp{margin:0;padding:0px;}
.bc{border-collapse:collapse;}
</style>
</head>
<body class=no_mp style="overflow:hidden;">
<table class=max>
<tr height=1>
<td>
<select id=selDir></select>
<button id=btn追加>追加</button>
<button id=btn保存>保存</button>
<button id=btn削除>削除</button>
 
iframe用機能:
<button id=btnHTML>html</button>
<button id=btnEditable>編集可</button>
<button id=btnClickハンドラ>clickハンドラ設置</button>
</td>
</tr>
<tr><td><iframe id=ifrVIEW class="max" style="position:relative;left:-3px;"></iframe></td></tr>
<tr height=1>
<td>
<table id=tblElem class=max>
<tr><td>選択中エレメント用機能:<select id=selFun></select></td></tr>
<tr><td id=tdElem></td></tr>
</table>
</td>
</tr>
</table>
</body>
<script>
elem選択中 = null
ifrVIEW.document.body.contentEditable = true
ifrClickイベントハンドラ設置=function(){
// iframe内に画像ファイルなどをD&Dするとイベントハンドラが消えてしまうので、再定義する方法が必要。
ifrVIEW.document.body.onclick=function(){
elem選択中 = ifrVIEW.window.event.target
選択中エレメント向け機能の実行()
}
}
ifrClickイベントハンドラ設置()
btnClickハンドラ.onclick = ifrClickイベントハンドラ設置
btn追加.onclick=function(){
var name=prompt('新しいデータの名前を入力してください','new name'), path=基準フォルダ+'/データ/'+name
if(!name){return}
if(!addデータdir(name)){return}
selDir更新(name)
selDir.onchange()
}
addデータdir=function(name){
var path=基準フォルダ+'/データ/'+name
try{fs.CreateFolder(path)}catch(e){return alert('ファイルやフォルダの名前に使用できない文字が含まれています。\n\n'+name)}
Write(path+'/html.htm', '')
// エラー無く完了したらtrueを返す。
return true
}
btn保存.onclick=function(){ Write(selOps(selDir).value+'/html.htm', ifrVIEW.document.body[iH]) }
btn削除.onclick=function(){
var ops=selOps(selDir)
if(!confirm('['+ops.text+']を削除しますか?')){return}
fs.DeleteFolder(ops.value)
selDir.options.length==1 && addデータdir('new name')
selDir更新()
selDir.onchange()
}
selDir更新=function(selectName){
var path=基準フォルダ+'/データ'
var arr=getDirList(path), ops=selDir.options
while(ops[0]){ops[0] = null}
for0L(arr, function(i, name){
ops[i] = new Option(name, path+'/'+name)
if(name==selectName){ selDir[sI] = i }
})
}
btnEditable.onclick=function(){
if(this[iT]=='編集可'){
this[iT] = '編集不可'
ifrVIEW.document.body.contentEditable = false
}else{
this[iT] = '編集可'
ifrVIEW.document.body.contentEditable = true
}
}
btnHTML.onclick = function(){
var pathTMP=gsf2+'/html'+(new Date()).getTime()+'.html', div=document.createElement('DIV')
Write(pathTMP, ifrVIEW.document.body[iH])
document.body.insertBefore(div)
with(div.style){
position = 'absolute'
top = '0px'
left = '0px'
width = '100%'
height = '100%'
}
div[iH] = '<table class=max><td style="text-align:center;background-color:rgba(220,220,220,0.9);">メモ帳を閉じると操作可能になります。</td></table>'
shell.run('notepad '+fun(pathTMP), 1, true)
div.removeNode(true)
if(!fs.FileExists(pathTMP)){return}
ifrVIEW.document.body[iH] = Read(pathTMP)
fs.DeleteFile(pathTMP)
elem選択中 = null
}
selDir.onchange = function(){ var si=this[sI]; 0 <= si ? ifrVIEW.document.body[iH] = Read(this.options[si].value+'/html.htm') : 0 }
selFun.onchange = function(){ elem選択中 && 選択中エレメント向け機能の実行() }
選択中エレメント向け機能の実行 = function(){
var fun, path=selOps(selFun).value
eval('fun = '+Read(path+'/func.js'))
fun(elem選択中, tdElem, path)
}
onload = function(){
/*
var arr=[]
forIn(tblElem.ownerDocument.parentWindow, function(name,v){arr.push(name)})
Write(gsf2+'/aaa.txt',arr.sort().join('\r\n'))
shell.run('notepad '+gsf2+'/aaa.txt')
*/
基準フォルダ = fs.GetParentFolderName(location.href.replace(/^file:/,'').replace(/^\/+([A-Z]:)/,'$1')).replace(/%20/g,' ')
var path=基準フォルダ+'/設定/viewエリア内の選択中エレメント向け機能'
var arr=getDirList(path), ops=selFun.options
for0L(arr, function(i, name){ ops[i] = new Option(name, path+'/'+name) })
if(!fs.FolderExists(基準フォルダ+'/データ')){
fs.CreateFolder(基準フォルダ+'/データ')
addデータdir('new name')
}
selDir更新()
selDir.onchange()
}
// 以下、基礎関数
AXO = function(name){return new ActiveXObject(name)}
fs = AXO('Scripting.FileSystemObject')
shell = AXO('WScript.Shell')
gsf2 = fs.GetSpecialFolder(2)
Read = function(p){var o=fs.OpenTextFile(p), s=o.AtEndOfStream?'':o.ReadAll(); o.Close(); return s}
Write = function(path,v){with(fs.CreateTextFile(path)){Write(v);Close()}}
RAS = function(p){return Read(p).split('\r\n')}
for0L = function(arr,fun){for(var i=0,L=arr.length,res;i<L;i++){if(res=fun(i,arr[i])){return res}}}
forIn = function(obj,fun){var name,res; for(name in obj){if(res=fun(name, obj[name])){return res}}}
fun = function(path){return path.indexOf(' ')<0 ? path : ('"'+path+'"')}
selOps= function(sel){return sel.options[sel[sI]]}
getDirList = function(path, swFile){
var pathTMP=gsf2+'/getDirList_'+(new Date()).getTime()+'.txt', arr
shell.Run('cmd /C dir /A'+(swFile?'-':'')+'D /B "'+path+'" > "'+pathTMP+'"' , 0 , true)
arr = fs.FileExists(pathTMP) ? RAS(pathTMP) : []
arr.pop()
try{fs.DeleteFile(pathTMP)}catch(e){}
return arr
}
getFileList=function(path){return getDirList(path, true)}
getEBTN=function(elem, name){return elem.getElementsByTagName(name)}
iT='innerText', iH='innerHTML', PA='parentNode', FI='firstChild', CN='childNodes', NE='nextSibling', PR='previousSibling', sI='selectedIndex'
</script>
</html>
function(elem, td, path){
td.innerHTML = '<span></span> <button>PA</button> <button>FI</button> <button>PR</button> <button>NE</button> <button>iT</button> <button>iH</button> <button>oH</button>'
var gEBTN=function(name){return td.getElementsByTagName(name)}
var span=gEBTN('span')[0]
var btns=gEBTN('button')
var func=function(elem){
if(!elem){return alert('指定された関係のエレメントがありません')}
elem選択中 = elem
span.innerText = 'tagName:' + elem.tagName
}
func(elem選択中)
btns[0].onclick=function(){ func(elem選択中.parentNode) }
btns[1].onclick=function(){ func(elem選択中.firstChild) }
btns[2].onclick=function(){ func(elem選択中.previousSibling) }
btns[3].onclick=function(){ func(elem選択中. nextSibling) }
btns[4].onclick=function(){ confirm(elem選択中.innerText) }
btns[5].onclick=function(){ confirm(elem選択中.innerHTML) }
btns[6].onclick=function(){ confirm(elem選択中.outerHTML) }
}
function(elem, td, path){
td.innerHTML = '<textarea class=max wrap=off style="height:100px;"></textarea>'
td.firstChild.value = elem.outerHTML
}
function(elem, td, path){
td.innerHTML = '<table class="bc w-max" border=1>'+
'<tr><td width=1><span style="white-space:nowrap;">プロパティ名</span></td><td><input class=w-max></td></tr>'+
'<tr><td>値</td><td><textarea class=max wrap=off style="height:100px;"></textarea></td></tr></table>'
var inp=getEBTN(td,'input')[0], ta=getEBTN(td,'textarea')[0]
var getP=function(elem, propStr){
var arr=propStr.split('.'), obj=elem, name=arr.pop()
for0L(arr, function(i, key){ obj = obj[key] })
return {
setP:function(v){ obj[name] = v },
getP:function( ){ return obj[name] }
}
}
inp.onchange=function(){ if(inp.value){ ta.value = getP(elem, inp.value).getP() } }
ta .onchange=function(){ if(inp.value){ getP(elem, inp.value).setP( ta.value ) } }
}
function(elem, td, path){
// elemはiframe内で最後にクリックされたエレメント, tdはこの機能のインターフェースを表示するためのエリア, pathはこのスクリプトファイルが入っているフォルダのPath
td[iH] = '<button>position:absoluteにする</button><button>position:relativeにする</button>'
td[FI].onclick=function(){
elem.style.position = 'absolute'
elem.draggable = false
elem.onmousedown = function(){
var win=elem.ownerDocument.parentWindow, e=win.event, x=e.x-elem.offsetLeft, y=e.y-elem.offsetTop
with(elem.ownerDocument.body){
onmousemove = function(){ var e=win.event; with(elem.style){top=e.y-y; left=e.x-x} }
onmouseup = function(){ this.onmousemove = null }
}
}
}
td[CN][1].onclick=function(){
elem.style.position = 'relative'
elem.onmousedown = null
}
}

「設定/viewエリア内の選択中エレメント向け機能」フォルダ内にフォルダを追加すると



「選択中エレメント用機能:」のリストに表示されます。



上記機能はiframe内で最後にクリックした要素に対して動作します。


フォルダ内の「func.js」を編集すると動作内容を変更できます。

0 件のコメント:

コメントを投稿