11

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'> &#9653;
    <span class='momnode_cl'>*<span class='momconn_cl'>
    <span class='momitemref_cl'>comment</span></span>
    (&#8220;<span class='momstring_cl'>some simple notice</span>&#8221;
     <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:

So I probably don't need contenteditable

Community
  • 1
  • 1
Basile Starynkevitch
  • 223,805
  • 18
  • 296
  • 547
  • A span can become a contentEditable element, if you set its `contentEditable` attribute to true. – Kaiido Nov 05 '15 at 08:54
  • But then, how can I restrict it to reject some user events notably pasting of arbitrary HTML sequence... I don't even know the complete set of user actions (even if I guess most of it)... – Basile Starynkevitch Nov 05 '15 at 08:55
  • you could listen to the `keydownEvent` and its `which` property. Then block the default behaviour. (`span.addEventListener('keydown', function(evt){if(evt.which===13)evt.preventDefault();}`) but this won't block pasting block of codes, so you may need instead get the `inputEvent` and check if the value of your span's `textContent`, contains undesirable content, then remove it. – Kaiido Nov 05 '15 at 08:59
  • @Kalido: that could be an interesting answer, if you improve it a little bit – Basile Starynkevitch Nov 05 '15 at 09:00
  • @BasileStarynkevitch If in your code you select \* to \*, meaning elements wrapping `.momitemref_cl` and press e.g backspace. What should happen? Should the `.momitemref_cl` element be removed or not? – A. Wolff Nov 05 '15 at 11:27
  • @A.Wolff, no, it should not. That is why I said "no backspace" in my question – Basile Starynkevitch Nov 05 '15 at 11:28
  • @BasileStarynkevitch Ya but i was guessing "no backspace" only for `.momitemref_cl` elements. Now that's complicating thing for sure... – A. Wolff Nov 05 '15 at 11:36
  • Have you tried setting your `` tag CSS to `display:block;` .. since the `span` tags default `display` property is `inline`. – Jonathan Marzullo Nov 24 '15 at 20:18
  • No, I want the `` to be inline! – Basile Starynkevitch Nov 24 '15 at 22:02

5 Answers5

4

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);
Jai
  • 74,255
  • 12
  • 74
  • 103
3

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'>&#9653; <span class='momnode_cl'>*<span class='momconn_cl'>
    <span class='momitemref_cl'>comment</span></span>(&#8220;<span class='momstring_cl'>some simple notice</span>&#8221; <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/>
A. Wolff
  • 74,033
  • 9
  • 94
  • 155
  • 3
    This. Is. It. Handling events on children inside contenteditables is the real tricky part. – Abhitalks Nov 05 '15 at 10:30
  • @Abhitalks Still i see you can delete `.momitemref_cl` elements if you set cursor inside other child of container or select it with mouse. I'm not sure what is expected then... Well, that's not really easy to handle all possible behaviours... :) – A. Wolff Nov 05 '15 at 10:38
  • @A.Wolff: Where and how did you find that trick about nested `contenteditable` and `focusin`/`focusout` events? I would never have guessed that. – Basile Starynkevitch Nov 05 '15 at 14:21
  • 1
    @BasileStarynkevitch By tests. Now checking the spec, seems relevant regarding [editing host](http://www.w3.org/TR/2008/WD-html5-20080610/editing.html): `If an element is editable and its parent element is not, or if an element is editable and it has no parent element, then the element is an editing host. Editable elements can be nested. User agents must make editing hosts focusable (which typically means they enter the tab order). An editing host can contain non-editable sections, these are handled as described below`. So here using `focusin/focusout` events is just a (bad/ugly) workaround. – A. Wolff Nov 05 '15 at 14:31
  • @BasileStarynkevitch That's said, this is the best regarding your issue i can get: http://jsfiddle.net/qqmmnxpm/ Works only on chrome *and* not in all cases (still able to make it bug in specific cases). So unfortunately, seems really really hard to get exactly your expected behaviour in across browser. I'm not sure there is any solution for that :( Maybe this could help you to get a solution: https://github.com/timdown/rangy/releases But if i'm right, this plugin is abandoned due to no cross browser solution – A. Wolff Nov 05 '15 at 14:32
2

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 onpasteand maybe onclick event handlers to catch non-supporting browsers (a.k.a IE).

Kaiido
  • 123,334
  • 13
  • 219
  • 285
2

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'> &#9653;
    <span class='momnode_cl'>*<span class='momconn_cl'>
    <span class='momitemref_cl'>comment</span></span>
    (&#8220;<span class='momstring_cl'>some simple notice</span>&#8221;
     <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'> &#9653;
    <span class='momnode_cl'>*<span class='momconn_cl'>
    <span class='momitemref_cl'>comment</span></span>
    (&#8220;<span class='momstring_cl'>some simple notice</span>&#8221;
     <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 spans 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.

Abhitalks
  • 27,721
  • 5
  • 58
  • 81
  • 1
    Doesn't work with e.g alphanumeric pad regarding keyCode range on my laptop. And still able to enter for example `&` and others specific character (~,^, etc...). I guess you would have better to validate it using a regex. Oh and btw, you aren't specifically targeting `.momitemref_cl` elements. My quick test show that nested element in contenteditable don't react to any keyboard event (on chrome at least), that's why in my answer i use a 'ugly' trick – A. Wolff Nov 05 '15 at 10:21
  • @A.Wolff: Yes. Thanks for pointing that out. Not handling shifts and ctrls. Need to check character. Shall update soon. And yes, elements inside contenteditable do not fire events which are instead captured by the contenteditable parent. This is why I bound the event to the div. Sure enough, you nailed the switching of the attribute. – Abhitalks Nov 05 '15 at 10:27
1

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.