Skip to content

Instantly share code, notes, and snippets.

@oroce
Last active November 22, 2016 20:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save oroce/d51cba8c1199c8c62d684ea67fbfb9ef to your computer and use it in GitHub Desktop.
Save oroce/d51cba8c1199c8c62d684ea67fbfb9ef to your computer and use it in GitHub Desktop.
esnextbin sketch
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>ESNextbin Sketch</title>
<!-- put additional styles and scripts here -->
<style>
body {
padding: 2em;
}
#editor-container .ql-editor {
padding: .5em;
border: 1px solid #ccc;
max-width: 50em;
min-height: 8em;
}
#editor-container p {margin-top: 0}
#editor-container .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">
im an editor, you can type into me
</div>
<ul class="completions">
</ul>
</div>
</body>
</html>
// 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);
});
{
"name": "esnextbin-sketch",
"version": "0.0.0",
"dependencies": {
"quill": "1.1.5",
"babel-runtime": "6.18.0"
}
}
'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);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment