changeset 2:683f56999fb3

'DOMNodeFocused' pseudo-event + Mutation Event Demo
author Fabien Cazenave <fabien@cazenave.cc>
date Mon, 19 Dec 2011 18:50:06 +0100
parents eb3679d92996
children 6ef82d6818f4
files index.xhtml script.js style.css
diffstat 3 files changed, 162 insertions(+), 102 deletions(-) [+]
line wrap: on
line diff
--- a/index.xhtml
+++ b/index.xhtml
@@ -1,11 +1,10 @@
-<?xml version="1.0" encoding="UTF-8"?>
 <!DOCTYPE html>
 <html xmlns="http://www.w3.org/1999/xhtml">
 <head>
+  <meta charset="UTF-8" />
   <title> barbecue </title>
-  <meta charset="UTF-8" />
   <link type="text/css" href="style.css" rel="stylesheet" />
-  <script type="text/javascript" src="script.js"/>
+  <script type="text/javascript" src="script.js"></script>
 </head>
 <body>
   <form id="toolbar">
@@ -85,7 +84,7 @@
     </dl>
     <pre id="execCommand" />
   </form>
-  <pre id="breadcrumb"> </pre>
+  <pre id="breadcrumb"> </pre>
 
   <section contenteditable="true">
     <h2> Go ahead, edit away! </h2>
--- a/script.js
+++ b/script.js
@@ -1,100 +1,173 @@
-//    -  Mutation Events:
-//    -  https://developer.mozilla.org/en/XUL/Events#Mutation_DOM_events
+/** Copyright (c) 2010-2011 Fabien Cazenave and Sonny Piers.
+  *
+  * Permission is hereby granted, free of charge, to any person obtaining a copy
+  * of this software and associated documentation files (the "Software"), to
+  * deal in the Software without restriction, including without limitation the
+  * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+  * sell copies of the Software, and to permit persons to whom the Software is
+  * furnished to do so, subject to the following conditions:
+  *
+  * The above copyright notice and this permission notice shall be included in
+  * all copies or substantial portions of the Software.
+  *
+  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+  * IN THE SOFTWARE.
+  */
 
-var gBreadCrumb = null;
-function updateBreadCrumb(node, action) {
-  if (!gBreadCrumb || !node || (node == document.body)) return;
+/**
+  * File          : barbecue.js
+  * Author        : Fabien Cazenave <fabien@cazenave.cc>
+  * Version       : 0.1
+  * License       : MIT
+  * Last Modified : 2011-12-19
+  */
+
+/** wysiwyg Editor
+  * expected compatibility: all browsers except IE<9.
+  * 
+  * This HTML content editor is not intended to compete with CKEditor or
+  * TinyMCE: it is designed to *test* the various implementations of
+  * contentEditable and execCommand in modern browsers.
+  *
+  * Note: a specific micro-format is expected for the format toolbar.
+  */
+!function(window, document, undefined) {
   var
-    body = document.body,
-    text = node.nodeName,
-    tmp  = node.parentNode;
-  while (tmp && tmp != body) {
-    text = tmp.nodeName + " > " + text;
-    tmp = tmp.parentNode;
+    gActiveEditor = null, // active editing host
+    gCommandDump  = null; // command dump field (debug)
+
+  function ExecCommand(toolbarElement) {
+    var argVal, argStr,
+      type    = toolbarElement.getAttribute("type"),
+      command = toolbarElement.getAttribute("data-command");
+
+    // get the execCommand argument according to the button type
+    switch (type) {
+      case "button":   // toolbar button: no argument
+        argVal = argStr = false;
+        break;
+      case "checkbox": // styleWithCSS: boolean argument
+        argVal = argStr = "" + toolbarElement.checked + "";
+        break;
+      default:         // <select> menu: string argument
+        if (!toolbarElement.selectedIndex) return;
+        argVal = toolbarElement.value;
+        argStr = "'" + argVal.replace("<", "&lt;").replace(">", "&gt;") + "'";
+        toolbarElement.selectedIndex = 0; // reset drop-down list
+    }
+
+    // send requested action and re-focus the editable element
+    document.execCommand(command, false, argVal);
+    if (gActiveEditor) gActiveEditor.focus();
+
+    // debug
+    gCommandDump.innerHTML = "document.execCommand('" + command + "', false, '" + argStr + "');";
   }
-  text = action + ": " + text;
-  gBreadCrumb.innerHTML = text;
-  console.log(text);
-}
+
+  window.addEventListener("DOMContentLoaded", function() {
+    var i,
+      buttons = document.querySelectorAll("*[data-command]"),
+      editors = document.querySelectorAll("*[contenteditable]");
 
-// retrieve the startContainer of the current selection
-var gLastFocusNode = null;
-function getSelectionStart() {
-  var node = document.getSelection().anchorNode;
-  return (node.nodeName == "#text" ? node.parentNode : node);
-}
-function onCaretMove(event, action) {
-  var selection = document.getSelection();
-  var node = selection.isCollapsed ? selection.focusNode
-           : selection.getRangeAt(0).commonAncestorContainer;
-  if (node != gLastFocusNode) {
-    updateBreadCrumb(node, action);
-    gLastFocusNode = node;
+    // initialize all toolbar buttons
+    for (i = 0; i < buttons.length; i++) {
+      buttons[i].onclick  = function() { ExecCommand(this); };
+      buttons[i].onchange = function() { ExecCommand(this); };
+    }
+
+    // remember last-focused editable element
+    for (i = 0; i < editors.length; i++)
+      editors[i].onfocus = function() { gActiveEditor = this; };
+
+    // debug
+    gCommandDump = document.querySelector("#execCommand");
+    ExecCommand(document.querySelector("#useCSS"));
+  }, false);
+} (this, document);
+
+/** DOMNodeFocused
+  * expected compatibility: all browsers except IE<9.
+  *
+  * This custom (non-standard) event is fired when a node in an editable element
+  * gets the user focus.
+  */
+!function(window, document, undefined) {
+  var gLastFocusNode = null;
+
+  function onCaretMove(event) {
+    var selection = document.getSelection();
+    var node = selection.isCollapsed ? selection.focusNode
+             : selection.getRangeAt(0).commonAncestorContainer;
+    if (node != gLastFocusNode) {
+      gLastFocusNode = node;
+      // fire a 'DOMNodeFocused' mutation event with bubbling to set the .target
+      //var evtObject = document.createEvent("MutationEvent");
+      //evtObject.initMutationEvent("DOMNodeFocused", true, false, node, "", "", "", 0);
+      // note that a standard event works fine, too :-)
+      var evtObject = document.createEvent("Event");
+      evtObject.initEvent("DOMNodeFocused", true, false);
+      node.dispatchEvent(evtObject);
+    }
   }
-}
 
-window.addEventListener("DOMContentLoaded", function() {
-  gBreadCrumb = document.querySelector("#breadcrumb");
+  // trigger a 'DOMNodeFocused' event when the user moves the caret
+  window.addEventListener("DOMContentLoaded", function() {
+    var i, editors = document.querySelectorAll("*[contenteditable]");
+    for (i = 0; i < editors.length; i++) {
+      editors[i].addEventListener("keyup",   onCaretMove, false);
+      editors[i].addEventListener("mouseup", onCaretMove, false);
+    }
+  }, false);
+} (this, document);
 
-  function onAttrModified(event) { updateBreadCrumb(event.target, "keypress"); }
-  function onNodeInserted(event) { updateBreadCrumb(event.target, "inserted"); }
-  function onNodeRemoved(event)  { updateBreadCrumb(event.target, "removed "); }
-
-  function onKeyUp(event) { onCaretMove(event, "keyup"); }
-  function onClick(event) { onCaretMove(event, "click"); }
+/** Mutation Events -- demo!
+  * expected compatibility: Firefox Aurora, possibly other modern browsers.
+  * reference: https://developer.mozilla.org/en/XUL/Events#Mutation_DOM_events
+  *            http://help.dottoro.com/ljifcdwx.php
+  * 
+  * SXE requires these (standard) mutation events:
+  *   - 'new':    DOMNodeInserted
+  *   - 'remove': DOMNodeRemoved
+  *   - 'set':    DOMSubtreeModified, DOMAttrModified, DOMCharacterDataModified
+  * 
+  * Note: webkit doesn't support DOMAttrModified
+  * https://bugs.webkit.org/show_bug.cgi?id=8191
+  */
+window.addEventListener("DOMContentLoaded", function() {
+  var breadcrumb = document.querySelector("#breadcrumb");
+  function updateBreadCrumb(event) {
+    if (!breadcrumb || !event || !event.target) return;
+    var
+      body = document.body,
+      node = event.target,
+      text = node.nodeName,
+      tmp  = node.parentNode;
+    while (tmp && tmp != body) {
+      text = tmp.nodeName + " > " + text;
+      tmp = tmp.parentNode;
+    }
+    text = event.type + ": " + text;
+    breadcrumb.innerHTML = text;
+    console.log(text);
+  }
 
   // listening to mutation events on 'document' freezes Firefox :-/
   var i, editors = document.querySelectorAll("*[contenteditable]");
   for (i = 0; i < editors.length; i++) {
-    // mutation events -- do we really need them?
-    editors[i].addEventListener("DOMAttrModified", onAttrModified, false);
-    editors[i].addEventListener("DOMNodeInserted", onNodeInserted, false);
-    editors[i].addEventListener("DOMNodeRemoved",  onNodeRemoved,  false);
-    // caret movements
-    //editors[i].addEventListener("textinput", onKeyUp, false);
-    editors[i].addEventListener("keyup",   onKeyUp, false);
-    editors[i].addEventListener("mouseup", onClick, false);
+    // set
+    editors[i].addEventListener("DOMAttrModified",          updateBreadCrumb, false);
+    editors[i].addEventListener("DOMSubtreeModified",       updateBreadCrumb, false);
+    editors[i].addEventListener("DOMCharacterDataModified", updateBreadCrumb, false);
+    // new, remove
+    editors[i].addEventListener("DOMNodeInserted", updateBreadCrumb, false);
+    editors[i].addEventListener("DOMNodeRemoved",  updateBreadCrumb, false);
+    // focus (non-standard but can be useful)
+    editors[i].addEventListener("DOMNodeFocused",  updateBreadCrumb, false);
   }
 }, false);
 
-
-
-// editor
-
-var
-  gActiveEditor = null, // active editing host
-  gCommandDump  = null; // command dump field
-function ExecCommand(toolbarElement) {
-  var argVal, argStr,
-    type    = toolbarElement.getAttribute("type"),
-    command = toolbarElement.getAttribute("data-command");
-  switch (type) {    // get the execCommand argument according to the button type
-    case "button":   // toolbar button: no argument
-      argVal = argStr = false;
-      break;
-    case "checkbox": // styleWithCSS: boolean argument
-      argVal = argStr = "" + toolbarElement.checked + "";
-      break;
-    default:         // <select> menu: string argument
-      if (!toolbarElement.selectedIndex) return;
-      argVal = toolbarElement.value;
-      argStr = "'" + argVal.replace("<", "&lt;").replace(">", "&gt;") + "'";
-      toolbarElement.selectedIndex = 0; // reset drop-down list
-  }
-  document.execCommand(command, false, argVal); // send requested action
-  if (gActiveEditor) gActiveEditor.focus();     // re-focus the editable element
-  gCommandDump.innerHTML = "document.execCommand('" + command + "', false, '" + argStr + "');";
-}
-window.addEventListener("DOMContentLoaded", function() {
-  var i,
-    buttons = document.querySelectorAll("*[data-command]"),
-    editors = document.querySelectorAll("*[contenteditable]");
-  for (i = 0; i < buttons.length; i++) {
-    buttons[i].onclick  = function() { ExecCommand(this); };
-    buttons[i].onchange = function() { ExecCommand(this); };
-  }
-  for (i = 0; i < editors.length; i++)
-    editors[i].onfocus = function() { gActiveEditor = this; };
-  gCommandDump = document.querySelector("#execCommand");
-  ExecCommand(document.querySelector("#useCSS"));
-}, false);
--- a/style.css
+++ b/style.css
@@ -1,15 +1,3 @@
-.attrModified { border: 1px dashed red; }
-.nodeModified { border: 1px dashed navy; }
-.nodeInserted { border: 1px dotted brown; }
-
-
-
-
-
-
-
-
-
 html {
   background: #111;
   color: #eee;