>

Tiny Editor

<div id="editor"></div>
<div class="template" style="display: none;">
  <div class="tiny-editor" x-style="width?: @{$.width}px; height?: @{$.height}px;">
    <div class="tiny-head">
      <a href="javascript:void(0);" class="tiny-button"
         x-for="cmd of commands" title&="cmd.title"
         x-class="active@: $.checkCmd(cmd.name) ^check;"
         x-style="background-image?: url(icons/&{cmd.icon}.png);"
         click+="$.execCmd(cmd.name);">
      </a>
      <select style="margin: 5px;" change+="$.onFontSizeChange(event);">
        <option value="12px">12px</option>
        <option value="15px" selected>15px</option>
        <option value="18px">18px</option>
        <option value="21px">21px</option>
        <option value="24px">24px</option>
        <option value="27px">27px</option>
        <option value="30px">30px</option>
        <option value="36px">36px</option>
      </select>
    </div>
    <div class="tiny-body" contenteditable="true" spellcheck="false"
         x-ref="body" x-style="color&: $.fontColor; width?: @{$.width - 20}px; height?: @{$.height - 50}px; padding: 10px;"></div>
  </div>
</div>
<script src="../../javascripts/exact-skin.js"></script>
<!--[if lt IE 9]>
<script src="../../javascripts/es5-shim-4.5.7.min.js"></script>
<script src="../../javascripts/es5-sham-4.5.7.min.js"></script>
<script src="../../javascripts/jquery-1.9.0.min.js"></script>
<script src="../../javascripts/exact-skin-jquery.js"></script>
<![endif]-->
<script src="../../javascripts/exact.js"></script>
<script src="tiny-editor.js"></script>
.tiny-editor {
  position: relative;
  border: 1px solid #ddd;
  font-family: "Helvetica Neue", Helvetica, Arial, "Microsoft Yahei", sans-serif;
}
.tiny-head {
  width: 100%;
  height: 30px;
  position: absolute;
  background-color: #ddd;
}
.tiny-head .active {
  background-color: #ccc;
}
.tiny-body {
  position: absolute;
  top: 30px;
  outline: none;
  overflow-y: auto;
  font-size: 15px;
  line-height: 1.5em;
}
.tiny-body p, .tiny-body li, .tiny-body div {
  margin: 0;
  padding: 0;
}
.tiny-body ul, .tiny-body ol {
  margin: 0;
  padding: 0 0 0 2em;
}
.tiny-button {
  float: left;
  width: 20px;
  height: 20px;
  margin: 0;
  padding: 5px;
  display: inline-block;
  /*border-right: 1px solid #ccc;*/
  cursor: pointer;
  line-height: 20px;
  text-align: center;
  text-decoration: none;
  background-position: 6px 6px;
  background-repeat: no-repeat;
}
function TinyEditor() {
  Exact.Component.apply(this, arguments);
}
Exact.defineClass({
  constructor: TinyEditor, extend: Exact.Component,
  statics: {
    template: Exact.Skin.query('.template .tiny-editor'),
    defaults: function() {
      return {
        fontColor: 'black',
        height: 300,
        width: 600
      }
    },
    resources: {
      commands: [
        {name: 'bold', icon: 'text-bold', title: 'bold'},
        {name: 'italic', icon: 'text-italic', title: 'italic'},
        {name: 'underline', icon: 'text-underline', title: 'underline'},
        {name: 'subscript', icon: 'text-subscript', title: 'subscript'},
        {name: 'superscript', icon: 'text-superscript', title: 'superscript'},
        {name: 'justifyLeft', icon: 'text-align-left', title: 'align left'},
        {name: 'justifyCenter',  icon: 'text-align-center', title: 'align center'},
        {name: 'justifyRight', icon: 'text-align-right', title: 'align right'},
        {name: 'insertOrderedList', icon: 'text-ordered-list', title: 'ordered list'},
        {name: 'insertUnorderedList', icon: 'text-bullets-list', title: 'bullets list'},
        {name: 'undo', icon: 'action-undo', title: 'undo'},
        {name: 'redo', icon: 'action-redo', title: 'redo'}
      ]
    }
  },
  ready: function() {
    this.onCmdCheck = this.onCmdCheck.bind(this);
    this.on('click', this.onCmdCheck);
    this.on('keyup', this.onCmdCheck);
    this.on('mouseup', this.onCmdCheck);
    this.body.on('paste', this.onPaste.bind(this));
  },
  execCmd: function(name, value) {
    document.execCommand(name, false, value);
  },
  checkCmd: function(name) {
    if (name === 'undo' || name === 'redo') {
      return document.queryCommandEnabled(name);
    }
    return document.queryCommandState(name);
  },
  getData: function getData(type) {
    var $body = this.body.$skin;
    if (type === 'html') {
      return $body.innerHTML;
    } else if (type === 'text') {
      return $body.textContent || $body.innerText;
    }
    return '';
  },
  setData: function setData(type, data) {
    if (type === 'html') {
      this.body.set('innerHTML', data);
    } else if (type === 'text') {
      this.body.set('innerHTML', filterText(data));
    }
  },
  onPaste: function(event) {
    event.preventDefault();
    if (event.clipboardData) {
      this.execCmd('insertHTML', filterText(event.clipboardData.getData('text/plain')))
    } else {
      var range,  html = filterText(window.clipboardData.getData("Text"));
      if (document.getSelection) {
        var frag = document.createDocumentFragment();
        var div = document.createElement('div');
        div.innerHTML = html;
        while (div.firstChild) {
          frag.appendChild(div.firstChild);
        }
        range = document.getSelection().getRangeAt(0);
        range.deleteContents();
        range.insertNode(frag);
      } else if (document.selection) {
        range = document.selection.createRange();
        range.pasteHTML(html);
      }
    }
  },
  onCmdCheck: function() {
    this.send('check');
  },
  onFontSizeChange: function(event) {
    this.body.style.set('fontSize', event.target.value);
  }
});
function filterText(text) {
  return text.replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/[ ]/g, '&nbsp;').replace(/\n/g, '<br>');
}
var editor = Exact.Component.create(TinyEditor, {fontColor: '#666'});
editor.setData('text', 'This editor should work well in IE8~11.');
editor.attach(Exact.Skin.query('#editor'));