Initial commit as of 2018-10-16
This commit is contained in:
12
assets/js/formats/align.js
Normal file
12
assets/js/formats/align.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import Parchment from 'parchment';
|
||||
|
||||
let config = {
|
||||
scope: Parchment.Scope.BLOCK,
|
||||
whitelist: ['right', 'center', 'justify']
|
||||
};
|
||||
|
||||
let AlignAttribute = new Parchment.Attributor.Attribute('align', 'align', config);
|
||||
let AlignClass = new Parchment.Attributor.Class('align', 'ql-align', config);
|
||||
let AlignStyle = new Parchment.Attributor.Style('align', 'text-align', config);
|
||||
|
||||
export { AlignAttribute, AlignClass, AlignStyle };
|
11
assets/js/formats/background.js
Normal file
11
assets/js/formats/background.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import Parchment from 'parchment';
|
||||
import { ColorAttributor } from './color';
|
||||
|
||||
let BackgroundClass = new Parchment.Attributor.Class('background', 'ql-bg', {
|
||||
scope: Parchment.Scope.INLINE
|
||||
});
|
||||
let BackgroundStyle = new ColorAttributor('background', 'background-color', {
|
||||
scope: Parchment.Scope.INLINE
|
||||
});
|
||||
|
||||
export { BackgroundClass, BackgroundStyle };
|
9
assets/js/formats/blockquote.js
Normal file
9
assets/js/formats/blockquote.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import Block from '../blots/block';
|
||||
|
||||
|
||||
class Blockquote extends Block {}
|
||||
Blockquote.blotName = 'blockquote';
|
||||
Blockquote.tagName = 'blockquote';
|
||||
|
||||
|
||||
export default Blockquote;
|
22
assets/js/formats/bold.js
Normal file
22
assets/js/formats/bold.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import Inline from '../blots/inline';
|
||||
|
||||
class Bold extends Inline {
|
||||
static create() {
|
||||
return super.create();
|
||||
}
|
||||
|
||||
static formats() {
|
||||
return true;
|
||||
}
|
||||
|
||||
optimize(context) {
|
||||
super.optimize(context);
|
||||
if (this.domNode.tagName !== this.statics.tagName[0]) {
|
||||
this.replaceWith(this.statics.blotName);
|
||||
}
|
||||
}
|
||||
}
|
||||
Bold.blotName = 'bold';
|
||||
Bold.tagName = ['STRONG', 'B'];
|
||||
|
||||
export default Bold;
|
118
assets/js/formats/code.js
Normal file
118
assets/js/formats/code.js
Normal file
@@ -0,0 +1,118 @@
|
||||
import Delta from 'quill-delta';
|
||||
import Parchment from 'parchment';
|
||||
import Block from '../blots/block';
|
||||
import Inline from '../blots/inline';
|
||||
import TextBlot from '../blots/text';
|
||||
|
||||
|
||||
class Code extends Inline {}
|
||||
Code.blotName = 'code';
|
||||
Code.tagName = 'CODE';
|
||||
|
||||
|
||||
class CodeBlock extends Block {
|
||||
static create(value) {
|
||||
let domNode = super.create(value);
|
||||
domNode.setAttribute('spellcheck', false);
|
||||
return domNode;
|
||||
}
|
||||
|
||||
static formats() {
|
||||
return true;
|
||||
}
|
||||
|
||||
delta() {
|
||||
let text = this.domNode.textContent;
|
||||
if (text.endsWith('\n')) { // Should always be true
|
||||
text = text.slice(0, -1);
|
||||
}
|
||||
return text.split('\n').reduce((delta, frag) => {
|
||||
return delta.insert(frag).insert('\n', this.formats());
|
||||
}, new Delta());
|
||||
}
|
||||
|
||||
format(name, value) {
|
||||
if (name === this.statics.blotName && value) return;
|
||||
let [text, ] = this.descendant(TextBlot, this.length() - 1);
|
||||
if (text != null) {
|
||||
text.deleteAt(text.length() - 1, 1);
|
||||
}
|
||||
super.format(name, value);
|
||||
}
|
||||
|
||||
formatAt(index, length, name, value) {
|
||||
if (length === 0) return;
|
||||
if (Parchment.query(name, Parchment.Scope.BLOCK) == null ||
|
||||
(name === this.statics.blotName && value === this.statics.formats(this.domNode))) {
|
||||
return;
|
||||
}
|
||||
let nextNewline = this.newlineIndex(index);
|
||||
if (nextNewline < 0 || nextNewline >= index + length) return;
|
||||
let prevNewline = this.newlineIndex(index, true) + 1;
|
||||
let isolateLength = nextNewline - prevNewline + 1;
|
||||
let blot = this.isolate(prevNewline, isolateLength);
|
||||
let next = blot.next;
|
||||
blot.format(name, value);
|
||||
if (next instanceof CodeBlock) {
|
||||
next.formatAt(0, index - prevNewline + length - isolateLength, name, value);
|
||||
}
|
||||
}
|
||||
|
||||
insertAt(index, value, def) {
|
||||
if (def != null) return;
|
||||
let [text, offset] = this.descendant(TextBlot, index);
|
||||
text.insertAt(offset, value);
|
||||
}
|
||||
|
||||
length() {
|
||||
let length = this.domNode.textContent.length;
|
||||
if (!this.domNode.textContent.endsWith('\n')) {
|
||||
return length + 1;
|
||||
}
|
||||
return length;
|
||||
}
|
||||
|
||||
newlineIndex(searchIndex, reverse = false) {
|
||||
if (!reverse) {
|
||||
let offset = this.domNode.textContent.slice(searchIndex).indexOf('\n');
|
||||
return offset > -1 ? searchIndex + offset : -1;
|
||||
} else {
|
||||
return this.domNode.textContent.slice(0, searchIndex).lastIndexOf('\n');
|
||||
}
|
||||
}
|
||||
|
||||
optimize(context) {
|
||||
if (!this.domNode.textContent.endsWith('\n')) {
|
||||
this.appendChild(Parchment.create('text', '\n'));
|
||||
}
|
||||
super.optimize(context);
|
||||
let next = this.next;
|
||||
if (next != null && next.prev === this &&
|
||||
next.statics.blotName === this.statics.blotName &&
|
||||
this.statics.formats(this.domNode) === next.statics.formats(next.domNode)) {
|
||||
next.optimize(context);
|
||||
next.moveChildren(this);
|
||||
next.remove();
|
||||
}
|
||||
}
|
||||
|
||||
replace(target) {
|
||||
super.replace(target);
|
||||
[].slice.call(this.domNode.querySelectorAll('*')).forEach(function(node) {
|
||||
let blot = Parchment.find(node);
|
||||
if (blot == null) {
|
||||
node.parentNode.removeChild(node);
|
||||
} else if (blot instanceof Parchment.Embed) {
|
||||
blot.remove();
|
||||
} else {
|
||||
blot.unwrap();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
CodeBlock.blotName = 'code-block';
|
||||
CodeBlock.tagName = 'PRE';
|
||||
CodeBlock.TAB = ' ';
|
||||
|
||||
|
||||
export { Code, CodeBlock as default };
|
21
assets/js/formats/color.js
Normal file
21
assets/js/formats/color.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import Parchment from 'parchment';
|
||||
|
||||
class ColorAttributor extends Parchment.Attributor.Style {
|
||||
value(domNode) {
|
||||
let value = super.value(domNode);
|
||||
if (!value.startsWith('rgb(')) return value;
|
||||
value = value.replace(/^[^\d]+/, '').replace(/[^\d]+$/, '');
|
||||
return '#' + value.split(',').map(function(component) {
|
||||
return ('00' + parseInt(component).toString(16)).slice(-2);
|
||||
}).join('');
|
||||
}
|
||||
}
|
||||
|
||||
let ColorClass = new Parchment.Attributor.Class('color', 'ql-color', {
|
||||
scope: Parchment.Scope.INLINE
|
||||
});
|
||||
let ColorStyle = new ColorAttributor('color', 'color', {
|
||||
scope: Parchment.Scope.INLINE
|
||||
});
|
||||
|
||||
export { ColorAttributor, ColorClass, ColorStyle };
|
12
assets/js/formats/direction.js
Normal file
12
assets/js/formats/direction.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import Parchment from 'parchment';
|
||||
|
||||
let config = {
|
||||
scope: Parchment.Scope.BLOCK,
|
||||
whitelist: ['rtl']
|
||||
};
|
||||
|
||||
let DirectionAttribute = new Parchment.Attributor.Attribute('direction', 'dir', config);
|
||||
let DirectionClass = new Parchment.Attributor.Class('direction', 'ql-direction', config);
|
||||
let DirectionStyle = new Parchment.Attributor.Style('direction', 'direction', config);
|
||||
|
||||
export { DirectionAttribute, DirectionClass, DirectionStyle };
|
18
assets/js/formats/font.js
Normal file
18
assets/js/formats/font.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import Parchment from 'parchment';
|
||||
|
||||
let config = {
|
||||
scope: Parchment.Scope.INLINE,
|
||||
whitelist: ['serif', 'monospace']
|
||||
};
|
||||
|
||||
let FontClass = new Parchment.Attributor.Class('font', 'ql-font', config);
|
||||
|
||||
class FontStyleAttributor extends Parchment.Attributor.Style {
|
||||
value(node) {
|
||||
return super.value(node).replace(/["']/g, '');
|
||||
}
|
||||
}
|
||||
|
||||
let FontStyle = new FontStyleAttributor('font', 'font-family', config);
|
||||
|
||||
export { FontStyle, FontClass };
|
13
assets/js/formats/header.js
Normal file
13
assets/js/formats/header.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import Block from '../blots/block';
|
||||
|
||||
|
||||
class Header extends Block {
|
||||
static formats(domNode) {
|
||||
return this.tagName.indexOf(domNode.tagName) + 1;
|
||||
}
|
||||
}
|
||||
Header.blotName = 'header';
|
||||
Header.tagName = ['H1', 'H2', 'H3', 'H4', 'H5', 'H6'];
|
||||
|
||||
|
||||
export default Header;
|
57
assets/js/formats/image.js
Normal file
57
assets/js/formats/image.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import Parchment from 'parchment';
|
||||
import { sanitize } from '../formats/link';
|
||||
|
||||
const ATTRIBUTES = [
|
||||
'alt',
|
||||
'height',
|
||||
'width'
|
||||
];
|
||||
|
||||
|
||||
class Image extends Parchment.Embed {
|
||||
static create(value) {
|
||||
let node = super.create(value);
|
||||
if (typeof value === 'string') {
|
||||
node.setAttribute('src', this.sanitize(value));
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
static formats(domNode) {
|
||||
return ATTRIBUTES.reduce(function(formats, attribute) {
|
||||
if (domNode.hasAttribute(attribute)) {
|
||||
formats[attribute] = domNode.getAttribute(attribute);
|
||||
}
|
||||
return formats;
|
||||
}, {});
|
||||
}
|
||||
|
||||
static match(url) {
|
||||
return /\.(jpe?g|gif|png)$/.test(url) || /^data:image\/.+;base64/.test(url);
|
||||
}
|
||||
|
||||
static sanitize(url) {
|
||||
return sanitize(url, ['http', 'https', 'data']) ? url : '//:0';
|
||||
}
|
||||
|
||||
static value(domNode) {
|
||||
return domNode.getAttribute('src');
|
||||
}
|
||||
|
||||
format(name, value) {
|
||||
if (ATTRIBUTES.indexOf(name) > -1) {
|
||||
if (value) {
|
||||
this.domNode.setAttribute(name, value);
|
||||
} else {
|
||||
this.domNode.removeAttribute(name);
|
||||
}
|
||||
} else {
|
||||
super.format(name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
Image.blotName = 'image';
|
||||
Image.tagName = 'IMG';
|
||||
|
||||
|
||||
export default Image;
|
31
assets/js/formats/indent.js
Normal file
31
assets/js/formats/indent.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import Parchment from 'parchment';
|
||||
|
||||
class IdentAttributor extends Parchment.Attributor.Class {
|
||||
add(node, value) {
|
||||
if (value === '+1' || value === '-1') {
|
||||
let indent = this.value(node) || 0;
|
||||
value = (value === '+1' ? (indent + 1) : (indent - 1));
|
||||
}
|
||||
if (value === 0) {
|
||||
this.remove(node);
|
||||
return true;
|
||||
} else {
|
||||
return super.add(node, value);
|
||||
}
|
||||
}
|
||||
|
||||
canAdd(node, value) {
|
||||
return super.canAdd(node, value) || super.canAdd(node, parseInt(value));
|
||||
}
|
||||
|
||||
value(node) {
|
||||
return parseInt(super.value(node)) || undefined; // Don't return NaN
|
||||
}
|
||||
}
|
||||
|
||||
let IndentClass = new IdentAttributor('indent', 'ql-indent', {
|
||||
scope: Parchment.Scope.BLOCK,
|
||||
whitelist: [1, 2, 3, 4, 5, 6, 7, 8]
|
||||
});
|
||||
|
||||
export { IndentClass };
|
7
assets/js/formats/italic.js
Normal file
7
assets/js/formats/italic.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import Bold from './bold';
|
||||
|
||||
class Italic extends Bold { }
|
||||
Italic.blotName = 'italic';
|
||||
Italic.tagName = ['EM', 'I'];
|
||||
|
||||
export default Italic;
|
41
assets/js/formats/link.js
Normal file
41
assets/js/formats/link.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import Inline from '../blots/inline';
|
||||
|
||||
|
||||
class Link extends Inline {
|
||||
static create(value) {
|
||||
let node = super.create(value);
|
||||
value = this.sanitize(value);
|
||||
node.setAttribute('href', value);
|
||||
node.setAttribute('target', '_blank');
|
||||
return node;
|
||||
}
|
||||
|
||||
static formats(domNode) {
|
||||
return domNode.getAttribute('href');
|
||||
}
|
||||
|
||||
static sanitize(url) {
|
||||
return sanitize(url, this.PROTOCOL_WHITELIST) ? url : this.SANITIZED_URL;
|
||||
}
|
||||
|
||||
format(name, value) {
|
||||
if (name !== this.statics.blotName || !value) return super.format(name, value);
|
||||
value = this.constructor.sanitize(value);
|
||||
this.domNode.setAttribute('href', value);
|
||||
}
|
||||
}
|
||||
Link.blotName = 'link';
|
||||
Link.tagName = 'A';
|
||||
Link.SANITIZED_URL = 'about:blank';
|
||||
Link.PROTOCOL_WHITELIST = ['http', 'https', 'mailto', 'tel'];
|
||||
|
||||
|
||||
function sanitize(url, protocols) {
|
||||
let anchor = document.createElement('a');
|
||||
anchor.href = url;
|
||||
let protocol = anchor.href.slice(0, anchor.href.indexOf(':'));
|
||||
return protocols.indexOf(protocol) > -1;
|
||||
}
|
||||
|
||||
|
||||
export { Link as default, sanitize };
|
130
assets/js/formats/list.js
Normal file
130
assets/js/formats/list.js
Normal file
@@ -0,0 +1,130 @@
|
||||
import Parchment from 'parchment';
|
||||
import Block from '../blots/block';
|
||||
import Container from '../blots/container';
|
||||
|
||||
|
||||
class ListItem extends Block {
|
||||
static formats(domNode) {
|
||||
return domNode.tagName === this.tagName ? undefined : super.formats(domNode);
|
||||
}
|
||||
|
||||
format(name, value) {
|
||||
if (name === List.blotName && !value) {
|
||||
this.replaceWith(Parchment.create(this.statics.scope));
|
||||
} else {
|
||||
super.format(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
remove() {
|
||||
if (this.prev == null && this.next == null) {
|
||||
this.parent.remove();
|
||||
} else {
|
||||
super.remove();
|
||||
}
|
||||
}
|
||||
|
||||
replaceWith(name, value) {
|
||||
this.parent.isolate(this.offset(this.parent), this.length());
|
||||
if (name === this.parent.statics.blotName) {
|
||||
this.parent.replaceWith(name, value);
|
||||
return this;
|
||||
} else {
|
||||
this.parent.unwrap();
|
||||
return super.replaceWith(name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
ListItem.blotName = 'list-item';
|
||||
ListItem.tagName = 'LI';
|
||||
|
||||
|
||||
class List extends Container {
|
||||
static create(value) {
|
||||
let tagName = value === 'ordered' ? 'OL' : 'UL';
|
||||
let node = super.create(tagName);
|
||||
if (value === 'checked' || value === 'unchecked') {
|
||||
node.setAttribute('data-checked', value === 'checked');
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
static formats(domNode) {
|
||||
if (domNode.tagName === 'OL') return 'ordered';
|
||||
if (domNode.tagName === 'UL') {
|
||||
if (domNode.hasAttribute('data-checked')) {
|
||||
return domNode.getAttribute('data-checked') === 'true' ? 'checked' : 'unchecked';
|
||||
} else {
|
||||
return 'bullet';
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
constructor(domNode) {
|
||||
super(domNode);
|
||||
const listEventHandler = (e) => {
|
||||
if (e.target.parentNode !== domNode) return;
|
||||
let format = this.statics.formats(domNode);
|
||||
let blot = Parchment.find(e.target);
|
||||
if (format === 'checked') {
|
||||
blot.format('list', 'unchecked');
|
||||
} else if(format === 'unchecked') {
|
||||
blot.format('list', 'checked');
|
||||
}
|
||||
}
|
||||
|
||||
domNode.addEventListener('touchstart', listEventHandler);
|
||||
domNode.addEventListener('mousedown', listEventHandler);
|
||||
}
|
||||
|
||||
format(name, value) {
|
||||
if (this.children.length > 0) {
|
||||
this.children.tail.format(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
formats() {
|
||||
// We don't inherit from FormatBlot
|
||||
return { [this.statics.blotName]: this.statics.formats(this.domNode) };
|
||||
}
|
||||
|
||||
insertBefore(blot, ref) {
|
||||
if (blot instanceof ListItem) {
|
||||
super.insertBefore(blot, ref);
|
||||
} else {
|
||||
let index = ref == null ? this.length() : ref.offset(this);
|
||||
let after = this.split(index);
|
||||
after.parent.insertBefore(blot, after);
|
||||
}
|
||||
}
|
||||
|
||||
optimize(context) {
|
||||
super.optimize(context);
|
||||
let next = this.next;
|
||||
if (next != null && next.prev === this &&
|
||||
next.statics.blotName === this.statics.blotName &&
|
||||
next.domNode.tagName === this.domNode.tagName &&
|
||||
next.domNode.getAttribute('data-checked') === this.domNode.getAttribute('data-checked')) {
|
||||
next.moveChildren(this);
|
||||
next.remove();
|
||||
}
|
||||
}
|
||||
|
||||
replace(target) {
|
||||
if (target.statics.blotName !== this.statics.blotName) {
|
||||
let item = Parchment.create(this.statics.defaultChild);
|
||||
target.moveChildren(item);
|
||||
this.appendChild(item);
|
||||
}
|
||||
super.replace(target);
|
||||
}
|
||||
}
|
||||
List.blotName = 'list';
|
||||
List.scope = Parchment.Scope.BLOCK_BLOT;
|
||||
List.tagName = ['OL', 'UL'];
|
||||
List.defaultChild = 'list-item';
|
||||
List.allowedChildren = [ListItem];
|
||||
|
||||
|
||||
export { ListItem, List as default };
|
23
assets/js/formats/script.js
Normal file
23
assets/js/formats/script.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import Inline from '../blots/inline';
|
||||
|
||||
class Script extends Inline {
|
||||
static create(value) {
|
||||
if (value === 'super') {
|
||||
return document.createElement('sup');
|
||||
} else if (value === 'sub') {
|
||||
return document.createElement('sub');
|
||||
} else {
|
||||
return super.create(value);
|
||||
}
|
||||
}
|
||||
|
||||
static formats(domNode) {
|
||||
if (domNode.tagName === 'SUB') return 'sub';
|
||||
if (domNode.tagName === 'SUP') return 'super';
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
Script.blotName = 'script';
|
||||
Script.tagName = ['SUB', 'SUP'];
|
||||
|
||||
export default Script;
|
12
assets/js/formats/size.js
Normal file
12
assets/js/formats/size.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import Parchment from 'parchment';
|
||||
|
||||
let SizeClass = new Parchment.Attributor.Class('size', 'ql-size', {
|
||||
scope: Parchment.Scope.INLINE,
|
||||
whitelist: ['small', 'large', 'huge']
|
||||
});
|
||||
let SizeStyle = new Parchment.Attributor.Style('size', 'font-size', {
|
||||
scope: Parchment.Scope.INLINE,
|
||||
whitelist: ['10px', '18px', '32px']
|
||||
});
|
||||
|
||||
export { SizeClass, SizeStyle };
|
7
assets/js/formats/strike.js
Normal file
7
assets/js/formats/strike.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import Inline from '../blots/inline';
|
||||
|
||||
class Strike extends Inline { }
|
||||
Strike.blotName = 'strike';
|
||||
Strike.tagName = 'S';
|
||||
|
||||
export default Strike;
|
7
assets/js/formats/underline.js
Normal file
7
assets/js/formats/underline.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import Inline from '../blots/inline';
|
||||
|
||||
class Underline extends Inline { }
|
||||
Underline.blotName = 'underline';
|
||||
Underline.tagName = 'U';
|
||||
|
||||
export default Underline;
|
53
assets/js/formats/video.js
Normal file
53
assets/js/formats/video.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import { BlockEmbed } from '../blots/block';
|
||||
import Link from '../formats/link';
|
||||
|
||||
const ATTRIBUTES = [
|
||||
'height',
|
||||
'width'
|
||||
];
|
||||
|
||||
|
||||
class Video extends BlockEmbed {
|
||||
static create(value) {
|
||||
let node = super.create(value);
|
||||
node.setAttribute('frameborder', '0');
|
||||
node.setAttribute('allowfullscreen', true);
|
||||
node.setAttribute('src', this.sanitize(value));
|
||||
return node;
|
||||
}
|
||||
|
||||
static formats(domNode) {
|
||||
return ATTRIBUTES.reduce(function(formats, attribute) {
|
||||
if (domNode.hasAttribute(attribute)) {
|
||||
formats[attribute] = domNode.getAttribute(attribute);
|
||||
}
|
||||
return formats;
|
||||
}, {});
|
||||
}
|
||||
|
||||
static sanitize(url) {
|
||||
return Link.sanitize(url);
|
||||
}
|
||||
|
||||
static value(domNode) {
|
||||
return domNode.getAttribute('src');
|
||||
}
|
||||
|
||||
format(name, value) {
|
||||
if (ATTRIBUTES.indexOf(name) > -1) {
|
||||
if (value) {
|
||||
this.domNode.setAttribute(name, value);
|
||||
} else {
|
||||
this.domNode.removeAttribute(name);
|
||||
}
|
||||
} else {
|
||||
super.format(name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
Video.blotName = 'video';
|
||||
Video.className = 'ql-video';
|
||||
Video.tagName = 'IFRAME';
|
||||
|
||||
|
||||
export default Video;
|
Reference in New Issue
Block a user