Skip to content

Instantly share code, notes, and snippets.

@oroce
Last active November 22, 2016 20:05
Show Gist options
  • Select an option

  • Save oroce/d51cba8c1199c8c62d684ea67fbfb9ef to your computer and use it in GitHub Desktop.

Select an option

Save oroce/d51cba8c1199c8c62d684ea67fbfb9ef to your computer and use it in GitHub Desktop.

Revisions

  1. oroce revised this gist Nov 22, 2016. 1 changed file with 3 additions and 3 deletions.
    6 changes: 3 additions & 3 deletions index.html
    Original file line number Diff line number Diff line change
    @@ -9,16 +9,16 @@
    padding: 2em;
    }

    #editor .ql-editor {
    #editor-container .ql-editor {
    padding: .5em;
    border: 1px solid #ccc;
    max-width: 50em;
    min-height: 8em;
    }

    #editor p {margin-top: 0}
    #editor-container p {margin-top: 0}

    #editor .mention {
    #editor-container .mention {
    font-weight: bold;
    color: blue;
    }
  2. oroce revised this gist Nov 22, 2016. 2 changed files with 2 additions and 2 deletions.
    2 changes: 1 addition & 1 deletion esnextbin.md
    Original file line number Diff line number Diff line change
    @@ -1 +1 @@
    made with [esnextbin](http://esnextb.in)
    made with [esnextbin](http://esnextb.in/?gist=d51cba8c1199c8c62d684ea67fbfb9ef)
    2 changes: 1 addition & 1 deletion index.html
    Original file line number Diff line number Diff line change
    @@ -67,7 +67,7 @@
    <!-- put markup and other contents here -->
    <div style="position: relative">
    <div id="editor-container">

    im an editor, you can type into me
    </div>
    <ul class="completions">
    </ul>
  3. oroce created this gist Nov 22, 2016.
    1 change: 1 addition & 0 deletions esnextbin.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1 @@
    made with [esnextbin](http://esnextb.in)
    76 changes: 76 additions & 0 deletions index.html
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,76 @@
    <!doctype html>
    <html>
    <head>
    <meta charset="utf-8">
    <title>ESNextbin Sketch</title>
    <!-- put additional styles and scripts here -->
    <style>
    body {
    padding: 2em;
    }

    #editor .ql-editor {
    padding: .5em;
    border: 1px solid #ccc;
    max-width: 50em;
    min-height: 8em;
    }

    #editor p {margin-top: 0}

    #editor .mention {
    font-weight: bold;
    color: blue;
    }

    .completions {
    list-style: none;
    margin: 0;
    padding: 0;
    background: white;
    border-radius: 2px;
    box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.25);
    }
    .completions > li {
    margin: 0;
    padding: 0;
    }
    .completions > li > button {
    box-sizing: border-box;
    height: 2em;
    padding: .25em .5em;
    margin: 0;
    display: block;
    width: 100%;
    text-align: left;
    border: none;
    background: none;
    }
    .completions > li > button:hover {
    background: #ddd;
    }
    .completions > li > button:focus {
    background: #ddd;
    outline: none;
    }
    .completions > li > button > .matched {
    font-weight: bold;
    color: black;
    }
    .completions > li > button > * {
    vertical-align: "middle";
    }

    </style>
    </head>
    <body>
    <!-- put markup and other contents here -->
    <div style="position: relative">
    <div id="editor-container">

    </div>
    <ul class="completions">
    </ul>
    </div>
    </body>
    </html>
    268 changes: 268 additions & 0 deletions index.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,268 @@
    // write ES2015 code and import modules from npm
    // and then press "Execute" to run your program
    import Quill from 'quill';
    let BlockEmbed = Quill.import('blots/block/embed');
    class VideoBlot extends BlockEmbed {
    static create(url) {
    let node = super.create();

    // Set non-format related attributes with static values
    node.setAttribute('frameborder', '0');
    node.setAttribute('allowfullscreen', true);

    return node;
    }

    static formats(node) {
    // We still need to report unregistered embed formats
    let format = {};
    if (node.hasAttribute('height')) {
    format.height = node.getAttribute('height');
    }
    if (node.hasAttribute('width')) {
    format.width = node.getAttribute('width');
    }
    return format;
    }

    static value(node) {
    return node.getAttribute('src');
    }

    format(name, value) {
    // Handle unregistered embed formats
    if (name === 'height' || name === 'width') {
    if (value) {
    this.domNode.setAttribute(name, value);
    } else {
    this.domNode.removeAttribute(name, value);
    }
    } else {
    super.format(name, value);
    }
    }
    }
    VideoBlot.blotName = 'video';
    VideoBlot.tagName = 'iframe';
    //Quill.register(VideoBlot);


    /* Credits go to: http://codepen.io/anon/pen/MjNeVM */
    const Inline = Quill.import('blots/inline');

    class MentionBlot extends Inline {
    static create(id) {
    const node = super.create();
    node.dataset.id = id;
    return node;
    }
    static formats(node) {
    return node.dataset.id;
    }
    format(name, value) {
    if (name === "mention" && value) {
    this.domNode.dataset.id = value;
    } else {
    super.format(name, value);
    }
    }

    formats() {
    const formats = super.formats();
    formats['mention'] = MentionBlot.formats(this.domNode);
    return formats;
    }
    }

    MentionBlot.blotName = "mention";
    MentionBlot.tagName = "SPAN";
    MentionBlot.className = "mention";


    Quill.register({
    'formats/mention': MentionBlot
    });


    const h = (tag, attrs, ...children) => {
    const elem = document.createElement(tag);
    Object.keys(attrs).forEach(key => elem[key] = attrs[key]);
    children.forEach(child => {
    if (typeof child === "string")
    child = document.createTextNode(child);
    elem.appendChild(child);
    });
    return elem;
    };

    class Mentions {
    constructor(quill, props) {
    this.quill = quill;
    this.onClose = props.onClose;
    this.onOpen = props.onOpen;
    this.users = props.users;
    this.container = this.quill.container.parentNode.querySelector(props.container);
    this.container.style.position = "absolute";
    this.container.style.display = "none";

    this.onSelectionChange = this.maybeUnfocus.bind(this);
    this.onTextChange = this.update.bind(this);

    this.open = false;
    this.atIndex = null;
    this.focusedButton = null;

    quill.keyboard.addBinding({
    // TODO: Once Quill supports using event.key (#1091) use that instead of shift-2
    key: 50, // 2
    shiftKey: true,
    }, this.onAtKey.bind(this));
    quill.keyboard.addBinding({
    // TODO: Once Quill supports using event.key (#1091) use that instead of shift-2
    key: 81, // 2
    altKey: true,
    }, this.onAtKey.bind(this));

    quill.keyboard.addBinding({
    key: 40, // ArrowDown
    collapsed: true,
    format: ["mention"]
    }, this.handleArrow.bind(this));
    // TODO: Add keybindings for Enter (13) and Tab (9) directly on the quill editor
    }

    onAtKey(range, context) {
    if (this.open) return true;
    if (range.length > 0) {
    this.quill.deleteText(range.index, range.length, Quill.sources.USER);
    }
    this.quill.insertText(range.index, "@", "mention", "0", Quill.sources.USER);
    const atSignBounds = this.quill.getBounds(range.index);
    this.quill.setSelection(range.index + 1, Quill.sources.SILENT);

    this.atIndex = range.index;
    this.container.style.left = atSignBounds.left + "px";
    this.container.style.top = atSignBounds.top + atSignBounds.height + "px",
    this.open = true;

    this.quill.on('text-change', this.onTextChange);
    this.quill.once('selection-change', this.onSelectionChange);
    this.update();
    this.onOpen && this.onOpen();
    }

    handleArrow() {
    if (!this.open) return true;
    this.buttons[0].focus();
    }

    update() {
    const sel = this.quill.getSelection().index;
    if (this.atIndex >= sel) { // Deleted the at character
    return this.close(null);
    }
    this.query = this.quill.getText(this.atIndex + 1, sel - this.atIndex - 1);
    // TODO: Should use fuse.js or similar fuzzy-matcher
    const users = this.users
    .filter(u => u.name.startsWith(this.query))
    .sort((u1, u2) => u1.name > u2.name);
    this.renderCompletions(users);
    }

    maybeUnfocus() {
    if (this.container.querySelector("*:focus")) return;
    this.close(null);
    }

    renderCompletions(users) {
    while (this.container.firstChild) this.container.removeChild(this.container.firstChild);
    const buttons = Array(users.length);
    this.buttons = buttons;
    const handler = (i, user) => event => {
    if (event.key === "ArrowDown" || event.keyCode === 40) {
    event.preventDefault();
    buttons[Math.min(buttons.length - 1, i + 1)].focus();
    } else if (event.key === "ArrowUp" || event.keyCode === 38) {
    event.preventDefault();
    buttons[Math.max(0, i - 1)].focus();
    } else if (event.key === "Enter" || event.keyCode === 13
    || event.key === " " || event.keyCode === 32
    || event.key === "Tab" || event.keyCode === 9) {
    event.preventDefault();
    this.close(user);
    }
    };
    users.forEach((user, i) => {
    const li = h('li', {},
    h('button', {type: "button"},
    h('span', {className: "matched"}, "@" + this.query),
    h('span', {className: "unmatched"}, user.name.slice(this.query.length))));
    this.container.appendChild(li);
    buttons[i] = li.firstChild;
    // Events will be GC-ed with button on each re-render:
    buttons[i].addEventListener('keydown', handler(i, user));
    buttons[i].addEventListener("mousedown", () => this.close(user));
    buttons[i].addEventListener("focus", () => this.focusedButton = i);
    buttons[i].addEventListener("unfocus", () => this.focusedButton = null);
    });
    this.container.style.display = "block";
    }

    close(value) {
    this.container.style.display = "none";
    while (this.container.firstChild) this.container.removeChild(this.container.firstChild);
    this.quill.off('selection-change', this.onSelectionChange);
    this.quill.off('text-change', this.onTextChange);
    if (value) {
    const {id, name} = value;
    this.quill.deleteText(this.atIndex, this.query.length + 1, Quill.sources.USER);
    this.quill.insertText(this.atIndex, "@" + name, "mention", id, Quill.sources.USER);
    this.quill.insertText(this.atIndex + name.length + 1, " ", 'mention', false, Quill.sources.USER);
    this.quill.setSelection(this.atIndex + name.length + 1, 0, Quill.sources.SILENT);
    }
    this.quill.focus();
    this.open = false;
    this.onClose && this.onClose(value);
    }

    }

    Quill.register('modules/mentions', Mentions);


    var quill = new Quill('#editor-container', {
    modules: {
    mentions: {
    container: '.completions',
    onClose: val => console.log("Closing: ", val),
    onOpen: () => console.log("Opening"),
    users: [
    {id: 1, name: 'Christy'},
    {id: 2, name: 'Micha'},
    {id: 3, name: 'Sima'},
    {id: 4, name: 'Coreen'},
    {id: 5, name: 'Aimee'},
    {id: 6, name: 'Brant'},
    {id: 7, name: 'Maryetta'},
    {id: 8, name: 'Nicol'},
    {id: 9, name: 'Thresa'},
    {id: 10, name: 'Pura'},
    {id: 11, name: 'Audie'},
    {id: 12, name: 'Jacob'},
    {id: 13, name: 'Mika'},
    {id: 14, name: 'Nubia'},
    {id: 15, name: 'Ana'},
    {id: 16, name: 'Sudie'},
    {id: 17, name: 'Raymundo'},
    {id: 18, name: 'Carolyne'},
    {id: 19, name: 'Doretha'},
    {id: 20, name: 'Milo'},
    ]
    }
    }
    });


    quill.on('text-change', (dlt, oldDlt, source) => {
    console.log('text-change', dlt, oldDlt, source);
    });
    8 changes: 8 additions & 0 deletions package.json
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,8 @@
    {
    "name": "esnextbin-sketch",
    "version": "0.0.0",
    "dependencies": {
    "quill": "1.1.5",
    "babel-runtime": "6.18.0"
    }
    }
    340 changes: 340 additions & 0 deletions transpiled.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,340 @@
    'use strict';

    var _keys = require('babel-runtime/core-js/object/keys');

    var _keys2 = _interopRequireDefault(_keys);

    var _getPrototypeOf = require('babel-runtime/core-js/object/get-prototype-of');

    var _getPrototypeOf2 = _interopRequireDefault(_getPrototypeOf);

    var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck');

    var _classCallCheck3 = _interopRequireDefault(_classCallCheck2);

    var _createClass2 = require('babel-runtime/helpers/createClass');

    var _createClass3 = _interopRequireDefault(_createClass2);

    var _possibleConstructorReturn2 = require('babel-runtime/helpers/possibleConstructorReturn');

    var _possibleConstructorReturn3 = _interopRequireDefault(_possibleConstructorReturn2);

    var _get2 = require('babel-runtime/helpers/get');

    var _get3 = _interopRequireDefault(_get2);

    var _inherits2 = require('babel-runtime/helpers/inherits');

    var _inherits3 = _interopRequireDefault(_inherits2);

    var _quill = require('quill');

    var _quill2 = _interopRequireDefault(_quill);

    function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

    var BlockEmbed = _quill2.default.import('blots/block/embed'); // write ES2015 code and import modules from npm
    // and then press "Execute" to run your program

    var VideoBlot = function (_BlockEmbed) {
    (0, _inherits3.default)(VideoBlot, _BlockEmbed);

    function VideoBlot() {
    (0, _classCallCheck3.default)(this, VideoBlot);
    return (0, _possibleConstructorReturn3.default)(this, (0, _getPrototypeOf2.default)(VideoBlot).apply(this, arguments));
    }

    (0, _createClass3.default)(VideoBlot, [{
    key: 'format',
    value: function format(name, value) {
    // Handle unregistered embed formats
    if (name === 'height' || name === 'width') {
    if (value) {
    this.domNode.setAttribute(name, value);
    } else {
    this.domNode.removeAttribute(name, value);
    }
    } else {
    (0, _get3.default)((0, _getPrototypeOf2.default)(VideoBlot.prototype), 'format', this).call(this, name, value);
    }
    }
    }], [{
    key: 'create',
    value: function create(url) {
    var node = (0, _get3.default)((0, _getPrototypeOf2.default)(VideoBlot), 'create', this).call(this);

    // Set non-format related attributes with static values
    node.setAttribute('frameborder', '0');
    node.setAttribute('allowfullscreen', true);

    return node;
    }
    }, {
    key: 'formats',
    value: function formats(node) {
    // We still need to report unregistered embed formats
    var format = {};
    if (node.hasAttribute('height')) {
    format.height = node.getAttribute('height');
    }
    if (node.hasAttribute('width')) {
    format.width = node.getAttribute('width');
    }
    return format;
    }
    }, {
    key: 'value',
    value: function value(node) {
    return node.getAttribute('src');
    }
    }]);
    return VideoBlot;
    }(BlockEmbed);

    VideoBlot.blotName = 'video';
    VideoBlot.tagName = 'iframe';
    //Quill.register(VideoBlot);

    /* Credits go to: http://codepen.io/anon/pen/MjNeVM */
    var Inline = _quill2.default.import('blots/inline');

    var MentionBlot = function (_Inline) {
    (0, _inherits3.default)(MentionBlot, _Inline);

    function MentionBlot() {
    (0, _classCallCheck3.default)(this, MentionBlot);
    return (0, _possibleConstructorReturn3.default)(this, (0, _getPrototypeOf2.default)(MentionBlot).apply(this, arguments));
    }

    (0, _createClass3.default)(MentionBlot, [{
    key: 'format',
    value: function format(name, value) {
    if (name === "mention" && value) {
    this.domNode.dataset.id = value;
    } else {
    (0, _get3.default)((0, _getPrototypeOf2.default)(MentionBlot.prototype), 'format', this).call(this, name, value);
    }
    }
    }, {
    key: 'formats',
    value: function formats() {
    var formats = (0, _get3.default)((0, _getPrototypeOf2.default)(MentionBlot.prototype), 'formats', this).call(this);
    formats['mention'] = MentionBlot.formats(this.domNode);
    return formats;
    }
    }], [{
    key: 'create',
    value: function create(id) {
    var node = (0, _get3.default)((0, _getPrototypeOf2.default)(MentionBlot), 'create', this).call(this);
    node.dataset.id = id;
    return node;
    }
    }, {
    key: 'formats',
    value: function formats(node) {
    return node.dataset.id;
    }
    }]);
    return MentionBlot;
    }(Inline);

    MentionBlot.blotName = "mention";
    MentionBlot.tagName = "SPAN";
    MentionBlot.className = "mention";

    _quill2.default.register({
    'formats/mention': MentionBlot
    });

    var h = function h(tag, attrs) {
    for (var _len = arguments.length, children = Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) {
    children[_key - 2] = arguments[_key];
    }

    var elem = document.createElement(tag);
    (0, _keys2.default)(attrs).forEach(function (key) {
    return elem[key] = attrs[key];
    });
    children.forEach(function (child) {
    if (typeof child === "string") child = document.createTextNode(child);
    elem.appendChild(child);
    });
    return elem;
    };

    var Mentions = function () {
    function Mentions(quill, props) {
    (0, _classCallCheck3.default)(this, Mentions);

    this.quill = quill;
    this.onClose = props.onClose;
    this.onOpen = props.onOpen;
    this.users = props.users;
    this.container = this.quill.container.parentNode.querySelector(props.container);
    this.container.style.position = "absolute";
    this.container.style.display = "none";

    this.onSelectionChange = this.maybeUnfocus.bind(this);
    this.onTextChange = this.update.bind(this);

    this.open = false;
    this.atIndex = null;
    this.focusedButton = null;

    quill.keyboard.addBinding({
    // TODO: Once Quill supports using event.key (#1091) use that instead of shift-2
    key: 50, // 2
    shiftKey: true
    }, this.onAtKey.bind(this));
    quill.keyboard.addBinding({
    // TODO: Once Quill supports using event.key (#1091) use that instead of shift-2
    key: 81, // 2
    altKey: true
    }, this.onAtKey.bind(this));

    quill.keyboard.addBinding({
    key: 40, // ArrowDown
    collapsed: true,
    format: ["mention"]
    }, this.handleArrow.bind(this));
    // TODO: Add keybindings for Enter (13) and Tab (9) directly on the quill editor
    }

    (0, _createClass3.default)(Mentions, [{
    key: 'onAtKey',
    value: function onAtKey(range, context) {
    if (this.open) return true;
    if (range.length > 0) {
    this.quill.deleteText(range.index, range.length, _quill2.default.sources.USER);
    }
    this.quill.insertText(range.index, "@", "mention", "0", _quill2.default.sources.USER);
    var atSignBounds = this.quill.getBounds(range.index);
    this.quill.setSelection(range.index + 1, _quill2.default.sources.SILENT);

    this.atIndex = range.index;
    this.container.style.left = atSignBounds.left + "px";
    this.container.style.top = atSignBounds.top + atSignBounds.height + "px", this.open = true;

    this.quill.on('text-change', this.onTextChange);
    this.quill.once('selection-change', this.onSelectionChange);
    this.update();
    this.onOpen && this.onOpen();
    }
    }, {
    key: 'handleArrow',
    value: function handleArrow() {
    if (!this.open) return true;
    this.buttons[0].focus();
    }
    }, {
    key: 'update',
    value: function update() {
    var _this3 = this;

    var sel = this.quill.getSelection().index;
    if (this.atIndex >= sel) {
    // Deleted the at character
    return this.close(null);
    }
    this.query = this.quill.getText(this.atIndex + 1, sel - this.atIndex - 1);
    // TODO: Should use fuse.js or similar fuzzy-matcher
    var users = this.users.filter(function (u) {
    return u.name.startsWith(_this3.query);
    }).sort(function (u1, u2) {
    return u1.name > u2.name;
    });
    this.renderCompletions(users);
    }
    }, {
    key: 'maybeUnfocus',
    value: function maybeUnfocus() {
    if (this.container.querySelector("*:focus")) return;
    this.close(null);
    }
    }, {
    key: 'renderCompletions',
    value: function renderCompletions(users) {
    var _this4 = this;

    while (this.container.firstChild) {
    this.container.removeChild(this.container.firstChild);
    }var buttons = Array(users.length);
    this.buttons = buttons;
    var handler = function handler(i, user) {
    return function (event) {
    if (event.key === "ArrowDown" || event.keyCode === 40) {
    event.preventDefault();
    buttons[Math.min(buttons.length - 1, i + 1)].focus();
    } else if (event.key === "ArrowUp" || event.keyCode === 38) {
    event.preventDefault();
    buttons[Math.max(0, i - 1)].focus();
    } else if (event.key === "Enter" || event.keyCode === 13 || event.key === " " || event.keyCode === 32 || event.key === "Tab" || event.keyCode === 9) {
    event.preventDefault();
    _this4.close(user);
    }
    };
    };
    users.forEach(function (user, i) {
    var li = h('li', {}, h('button', { type: "button" }, h('span', { className: "matched" }, "@" + _this4.query), h('span', { className: "unmatched" }, user.name.slice(_this4.query.length))));
    _this4.container.appendChild(li);
    buttons[i] = li.firstChild;
    // Events will be GC-ed with button on each re-render:
    buttons[i].addEventListener('keydown', handler(i, user));
    buttons[i].addEventListener("mousedown", function () {
    return _this4.close(user);
    });
    buttons[i].addEventListener("focus", function () {
    return _this4.focusedButton = i;
    });
    buttons[i].addEventListener("unfocus", function () {
    return _this4.focusedButton = null;
    });
    });
    this.container.style.display = "block";
    }
    }, {
    key: 'close',
    value: function close(value) {
    this.container.style.display = "none";
    while (this.container.firstChild) {
    this.container.removeChild(this.container.firstChild);
    }this.quill.off('selection-change', this.onSelectionChange);
    this.quill.off('text-change', this.onTextChange);
    if (value) {
    var id = value.id;
    var name = value.name;

    this.quill.deleteText(this.atIndex, this.query.length + 1, _quill2.default.sources.USER);
    this.quill.insertText(this.atIndex, "@" + name, "mention", id, _quill2.default.sources.USER);
    this.quill.insertText(this.atIndex + name.length + 1, " ", 'mention', false, _quill2.default.sources.USER);
    this.quill.setSelection(this.atIndex + name.length + 1, 0, _quill2.default.sources.SILENT);
    }
    this.quill.focus();
    this.open = false;
    this.onClose && this.onClose(value);
    }
    }]);
    return Mentions;
    }();

    _quill2.default.register('modules/mentions', Mentions);

    var quill = new _quill2.default('#editor-container', {
    modules: {
    mentions: {
    container: '.completions',
    onClose: function onClose(val) {
    return console.log("Closing: ", val);
    },
    onOpen: function onOpen() {
    return console.log("Opening");
    },
    users: [{ id: 1, name: 'Christy' }, { id: 2, name: 'Micha' }, { id: 3, name: 'Sima' }, { id: 4, name: 'Coreen' }, { id: 5, name: 'Aimee' }, { id: 6, name: 'Brant' }, { id: 7, name: 'Maryetta' }, { id: 8, name: 'Nicol' }, { id: 9, name: 'Thresa' }, { id: 10, name: 'Pura' }, { id: 11, name: 'Audie' }, { id: 12, name: 'Jacob' }, { id: 13, name: 'Mika' }, { id: 14, name: 'Nubia' }, { id: 15, name: 'Ana' }, { id: 16, name: 'Sudie' }, { id: 17, name: 'Raymundo' }, { id: 18, name: 'Carolyne' }, { id: 19, name: 'Doretha' }, { id: 20, name: 'Milo' }]
    }
    }
    });

    quill.on('text-change', function (dlt, oldDlt, source) {
    console.log('text-change', dlt, oldDlt, source);
    });