可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
I'm coding the MELT monitor (free software, alpha stage, related to the GCC MELT domain specific language to customize GCC). It is using libonion to behave as a specialized web server, and I want it to become a syntax directed editor of some DSL I am designing. I'm speaking of commit 97d60053 if that matters. You could run it as ./monimelt -Dweb,run -W localhost.localdomain:8086
then open http://localhost.localdomain:8086/microedit.html in your browser.
I am emitting (thru file webroot/microedit.html
)
<h1>Micro Editing Monimelt</h1>
<div id='microedit_id' contenteditable='true'>*</div>
<hr/>
then some AJAX trickery is filling that #micredit_id
element with something containing stuff similar to:
<dd class='statval_cl' data-forattr='notice'> ▵
<span class='momnode_cl'>*<span class='momconn_cl'>
<span class='momitemref_cl'>comment</span></span>
(“<span class='momstring_cl'>some simple notice</span>”
<span class='momnode_cl'>*<span class='momconn_cl'>
<span class='momitemref_cl'>web_state</span></span>
(<span class='momnumber_cl'>2</span>)</span>
<span class='momitemval_cl'>hashset</span>
<span class='momset_cl'>{<span class='momitemref_cl'>microedit</span>
<span class='momitemref_cl'>the_agenda</span>}</span>
<span class='momtuple_cl'>[<span class='momitemref_cl'>web_session</span>
<span class='momitemref_cl empty_cl'>~</span>
<span class='momitemref_cl'>the_system</span>]</span>)</span> ;</dd>
Now, I want every <span>
of class momitemref_cl
to be sensitive to some keyboard (and perhaps mouse) events. However, the contenteditable
elements can be edited by many user actions (I don't even understand what is the entire list of such user actions....) and I only want these span elements to be responsive to a defined and restricted set of key presses (alphanumerical & space) and not be able to be user-changed otherwise (e.g. no punctuation characters, no "cut", no "paste", no backspace, no tab, etc...).
Is there a complete list of events (or user actions) that a contenteditable='true'
element can get and is reacting to?
How to disable most of these events or user actions (on keyboard & mouse) and react only to some (well defined) keyboard events?
Apparently, a <span>
element in a non-contenteditable
element cannot get any keyboard user action (because it cannot get the focus)...
I am targeting only recent HTML5 browsers, such as Firefox 38 or 42, or Chrome 47 etc... on Debian/Linux/x86-64 if that matters (so I really don't care about IE9)
PS. this is a related question, but not the same one.
PS2: Found the why contenteditable
is terrible blog page. Makes me almost cry... Also read about faking an editable control in browser Javascript (for CodeMirror). See also W3C draft internal document on Editing Explainer and edit events draft. Both W3C things are work in progress. W3C TR on UI events is still (nov.2015) a working draft. See also http://jsfiddle.net/8j6jea6p/ (which behaves differently in Chrome 46 and in Firefox 42 or 43 beta)
PS3: perhaps a contenteditable
is after all a bad idea. I am (sadly) considering using a canvas
(à la carota) and doing all the editing & drawing by hand-written javascript...
addenda:
(November 26th 2015)
By discussing privately with some Mozilla persons, I understood that:
contenteditable
is messy (so I rather avoid it), and is not anymore worked much in Firefox (for instance, even recent beta Firefox don't know about contenteditable='events'
, see nsGenericHTMLElement.h
file)
event bubbling and capturing matters a big lot
a normal <div>
(or <span>
) can be made focusable by giving it a tabindex
property
text selection API could be useful (but has some recent bugs)
So I probably don't need contenteditable
回答1:
You can do as such:
function validateInput(usrAct){
swich(usrAct){
case "paste":
// do something when pasted
break;
case "keydown":
// dosomething on keydown
break;
default:
//do something on default
break;
}
}
document.querySelectorAll('.momitemref_cl').addEventListener('input', function(e){
validateInput(e.type)
}, false);
回答2:
This snippet could be what you are looking for, making span.momitemref_cl
elements focusable but not tabbable and setting has contenteditable
. But as i'm testing it on chrome, contenteditable
inside any container with attribute contenteditable
set to true
, don't fire any keyboard event. So the trick could be on focus to set any container to not editable (and switch back on blur).
See e.g: (keypress and keydown events are both binded to handle some specific cases where keypress or keydown wouldn't be fired on specifc keys)
NOTE: has you seem to populate DIV with content dynamically, you could delegate it or bind event (& set tabindex attribute if changing it in HTML markup not a solution) once ajax request has completed.
$('#microedit_id .momitemref_cl').attr('tabindex', -1).prop('contenteditable', true).on('focusin focusout', function(e) {
$(this).parents('[contenteditable]').prop('contenteditable', e.type === "focusout");
}).on('keypress keydown paste cut', function(e) {
if (/[a-zA-Z0-9 ]/.test(String.fromCharCode(e.which))) return;
return false;
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<h1>Micro Editing Monimelt</h1>
<div id='microedit_id' contenteditable='true'>
<dd class='statval_cl' data-forattr='notice'>▵ <span class='momnode_cl'>*<span class='momconn_cl'>
<span class='momitemref_cl'>comment</span></span>(“<span class='momstring_cl'>some simple notice</span>” <span class='momnode_cl'>*<span class='momconn_cl'>
<span class='momitemref_cl'>web_state</span></span>(<span class='momnumber_cl'>2</span>)</span> <span class='momitemval_cl'>hashset</span>
<span class='momset_cl'>{<span class='momitemref_cl'>microedit</span>
<span class='momitemref_cl'>the_agenda</span>}</span> <span class='momtuple_cl'>[<span class='momitemref_cl'>web_session</span>
<span class='momitemref_cl empty_cl'>~</span>
<span class='momitemref_cl'>the_system</span>]</span>)</span>;</dd>
</div>
<hr/>
回答3:
First, HTMLElements
become contentEditableElements
when you set their contentEditable
attribute to true
.
Now, the best way to do your parsing IMO is to listen to the inputEvent
and check your element's textContent
:
s.addEventListener('input', validate, false);
function validate(evt) {
var badValues = ['bad', 'content'];
var span = this;
badValues.forEach(function(v) {
if (span.textContent.indexOf(v) > -1) {
// that's bad m..key
span.textContent = span.textContent.split(v).join('');
}
});
};
<span id="s" contentEditable="true">Hello</span>
Unfortunately, the input event isn't widely supported
so you may need to add onkeydown
and onpaste
and maybe onclick
event handlers to catch non-supporting browsers (a.k.a IE).
回答4:
Edit:
(Handles only the spans with the said class. Also handles the case, where you could go back from another span into a previous one and could delete it. Incorporates the idea of @AWolff for switching the contenteditable attribute on focus)
The overall idea remains the same as that of the previous version.
Fiddle: http://jsfiddle.net/abhitalks/gb0mbwLu/
Snippet:
var div = document.getElementById('microedit_id'),
spans = document.querySelectorAll('#microedit_id .momitemref_cl'),
commands = ['paste', 'cut'],
// whitelist is the keycodes for keypress event
whitelist = [{'range': true, 'start': '97', 'end': '122'}, // lower-case
{'range': true, 'start': '65', 'end': '90'}, // upper-case
{'range': true, 'start': '48', 'end': '57' } // numbers
],
// specialkeys is the keycodes for keydown event
specialKeys = [8, 9, 13, 46] // backspace, tab, enter, delete
;
div.addEventListener('keydown', handleFromOutside, false);
[].forEach.call(spans, function(span) {
span.setAttribute('contenteditable', true);
span.setAttribute('tabindex', '-1');
span.addEventListener('focus', handleFocus, false);
span.addEventListener('blur', handleBlur, false);
commands.forEach(function(cmd) {
span.addEventListener(cmd, function(e) {
e.preventDefault(); return false;
});
});
span.addEventListener('keypress', handlePress, false);
span.addEventListener('keydown', handleDown, false);
});
function handleFocus(e) { div.setAttribute('contenteditable', false); }
function handleBlur(e) { div.setAttribute('contenteditable', true); }
function handlePress(e) {
var allowed = false, key = e.keyCode;
whitelist.forEach(function(range) {
if (key && (key != '') && (range.start <= key) && (key <= range.end)) {
allowed = true;
}
});
if (! allowed) { e.preventDefault(); return false; }
}
function handleDown(e) {
var allowed = false, key = e.keyCode;
specialKeys.forEach(function(spl) {
if (key && (spl == key)) { e.preventDefault(); return false; }
});
}
function handleFromOutside(e) {
var key = e.keyCode, node = window.getSelection().anchorNode, prev, next;
node = (node.nodeType == 3 ? node.parentNode : node)
prev = node.previousSibling; next = node.nextSibling;
if (prev || next) {
if (node.className == 'momitemref_cl') {
if (specialKeys.indexOf(key) >= 0) {
e.preventDefault(); return false;
}
}
}
}
<h1>Micro Editing Monimelt</h1>
<div id='microedit_id' contenteditable='true'>
<dd class='statval_cl' data-forattr='notice'> ▵
<span class='momnode_cl'>*<span class='momconn_cl'>
<span class='momitemref_cl'>comment</span></span>
(“<span class='momstring_cl'>some simple notice</span>”
<span class='momnode_cl'>*<span class='momconn_cl'>
<span class='momitemref_cl'>web_state</span></span>
(<span class='momnumber_cl'>2</span>)</span>
<span class='momitemval_cl'>hashset</span>
<span class='momset_cl'>{<span class='momitemref_cl'>microedit</span>
<span class='momitemref_cl'>the_agenda</span>}</span>
<span class='momtuple_cl'>[<span class='momitemref_cl'>web_session</span>
<span class='momitemref_cl empty_cl'>~</span>
<span class='momitemref_cl'>the_system</span>]</span>)</span> ;</dd>
</div>
<hr/>
Apart from the usual handling of events on the spans and preventing / allowing the keys and/or commands from the white-lists and balck-lists; what this code does is to also check if the cursor or editing is currently being done on other spans which are not constrained. When selecting or moving using arrow keys from there into the target spans, we dis-allow special keys to prevent deletion etc.
function handleFromOutside(e) {
var key = e.keyCode, node = window.getSelection().anchorNode, prev, next;
node = (node.nodeType == 3 ? node.parentNode : node)
prev = node.previousSibling; next = node.nextSibling;
if (prev || next) {
if (node.className == 'momitemref_cl') {
if (specialKeys.indexOf(key) >= 0) {
e.preventDefault(); return false;
}
}
}
}
I could not get much time, and thus one problem still remains. And, that is to disallow commands as well like cut and paste while moving into the target spans from outside.
Older version for reference only:
You could maintain a white-list (or blacklist if number of commands allowed are higher) of all keystrokes that you want to allow. Similarly, also maintain a dictionary of all events that you want to block.
Then wire up the commands on your div
and use event.preventDefault()
to reject that action. Next up, wire up the keydown
event and use the whitelist to allow all keystrokes that are in the permissible ranges as defined above:
In the example below only numbers and alphabets will be allowed as per the first range and arrow keys (along with pageup/down and space) will be allowed as per the second range. Rest all actions are blocked / rejected.
You can then extend it further to your use-case. Try it out in the demo below.
Fiddle: http://jsfiddle.net/abhitalks/re7ucgra/
Snippet:
var div = document.getElementById('microedit_id'),
spans = document.querySelectorAll('#microedit_id span'),
commands = ['paste'],
whitelist = [ {'start': 48, 'end': 90}, {'start': 32, 'end': 40 }, ]
;
commands.forEach(function(cmd) {
div.addEventListener(cmd, function(e) {
e.preventDefault(); return false;
});
});
div.addEventListener('keydown', handleKeys, false);
function handleKeys(e) {
var allowed = false;
whitelist.forEach(function(range) {
if ((range.start <= e.keyCode) && (e.keyCode <= range.end)) {
allowed = true;
}
});
if (! allowed) { e.preventDefault(); return false; }
};
<h1>Micro Editing Monimelt</h1>
<div id='microedit_id' contenteditable='true'>
<dd class='statval_cl' data-forattr='notice'> ▵
<span class='momnode_cl'>*<span class='momconn_cl'>
<span class='momitemref_cl'>comment</span></span>
(“<span class='momstring_cl'>some simple notice</span>”
<span class='momnode_cl'>*<span class='momconn_cl'>
<span class='momitemref_cl'>web_state</span></span>
(<span class='momnumber_cl'>2</span>)</span>
<span class='momitemval_cl'>hashset</span>
<span class='momset_cl'>{<span class='momitemref_cl'>microedit</span>
<span class='momitemref_cl'>the_agenda</span>}</span>
<span class='momtuple_cl'>[<span class='momitemref_cl'>web_session</span>
<span class='momitemref_cl empty_cl'>~</span>
<span class='momitemref_cl'>the_system</span>]</span>)</span> ;</dd>
</div>
<hr/>
Edited, to fix the problem of not capturing special keys especially when shift was pressed and the same keyCode
is generated for keypress
. Added, keydown
for handling special keys.
Note: This is assuming that to happen on the entire div
. As I see in the question, there are only span
s and that too nested ones. There are no other elements. If there are other elements involved and you want to exempt those, then you will need to bind the event to those elements only. This is because, the events on children are captured by the parent when parent is contenteditable
and not fired on the children.
回答5:
A straightforward solution to your problem would be to listen on the keydown
event fired by the inner-most element and act accordingly. An exemplary code snippet can be found below:
HTML:
<div class="momitemref_cl" contenteditable="true">Foo Bar</div>
<input class="not-momitemref_cl"/>
<input class="momitemref_cl"/>
JS:
document.querySelectorAll('.momitemref_cl').forEach((el) => {
el.addEventListener('keydown', validateInput);
el.addEventListener('cut', e => e.preventDefault());
el.addEventListener('copy', e => e.preventDefault());
el.addEventListener('paste', e => e.preventDefault());
});
function validateInput(userAction) {
console.log(userAction);
if (userAction.ctrlKey) {
userAction.preventDefault();
return false;
}
let code = (userAction.keyCode ? userAction.keyCode : userAction.which);
if ((48 <= code && code <= 57 && !userAction.shiftKey) || (65 <= code && code <= 90) || (97 <= code && code <= 122) || code === 32) {
console.log(`Allowing keypress with code: ${code}`);
return true;
}
console.log(`Preventing keypress with code: ${code}`);
userAction.preventDefault();
return false;
}
This works for both <input>
elements as well as elements with the contenteditable
attribute set to true
.
JS Fiddle: https://jsfiddle.net/rsjw3c87/22/
EDIT: Also added additional checks to prevent right-click & copy/cut/paste. Disabling right-click directly via the contextmenu
event will not work as certain browsers & OSes disallow you from disabling that specific event.