Compare commits
17 Commits
develop
...
image-exam
Author | SHA1 | Date |
---|---|---|
John McLear | d9ca1efd4f | |
John McLear | b876e2dfe0 | |
John McLear | 1cad0a6707 | |
John McLear | 1cbaf2d272 | |
John McLear | c6b0c92061 | |
John McLear | f466bcd32f | |
John McLear | 5bedfa0ba1 | |
John McLear | 66109002ef | |
John McLear | 638bd9942e | |
John McLear | 183b765df0 | |
John McLear | e39e959347 | |
John McLear | 62906d72a2 | |
John McLear | f5cfb8b135 | |
John McLear | f0a9e6832f | |
John McLear | 1f667f86ef | |
John McLear | ef2a929016 | |
John McLear | b26548011c |
|
@ -31,45 +31,31 @@ const Changeset = require('./Changeset');
|
|||
const hooks = require('./pluginfw/hooks');
|
||||
const _ = require('./underscore');
|
||||
|
||||
function sanitizeUnicode(s) {
|
||||
return UNorm.nfc(s);
|
||||
}
|
||||
|
||||
function makeContentCollector(collectStyles, abrowser, apool, domInterface, className2Author) {
|
||||
abrowser = abrowser || {};
|
||||
// I don't like the above.
|
||||
const sanitizeUnicode = (s) => UNorm.nfc(s);
|
||||
|
||||
const makeContentCollector = function (
|
||||
collectStyles, abrowser, apool, domInterface, className2Author) {
|
||||
const dom = domInterface || {
|
||||
isNodeText(n) {
|
||||
return (n.nodeType == 3);
|
||||
},
|
||||
nodeTagName(n) {
|
||||
return n.tagName;
|
||||
},
|
||||
nodeValue(n) {
|
||||
return n.nodeValue;
|
||||
},
|
||||
nodeNumChildren(n) {
|
||||
isNodeText: (n) => (n.nodeType === 3),
|
||||
nodeTagName: (n) => n.tagName,
|
||||
nodeValue: (n) => n.nodeValue,
|
||||
nodeNumChildren: (n) => {
|
||||
if (n.childNodes == null) return 0;
|
||||
return n.childNodes.length;
|
||||
},
|
||||
nodeChild(n, i) {
|
||||
nodeChild: (n, i) => {
|
||||
if (n.childNodes.item == null) {
|
||||
return n.childNodes[i];
|
||||
}
|
||||
return n.childNodes.item(i);
|
||||
},
|
||||
nodeProp(n, p) {
|
||||
return n[p];
|
||||
},
|
||||
nodeAttr(n, a) {
|
||||
nodeProp: (n, p) => n[p],
|
||||
nodeAttr: (n, a) => {
|
||||
if (n.getAttribute != null) return n.getAttribute(a);
|
||||
if (n.attribs != null) return n.attribs[a];
|
||||
return null;
|
||||
},
|
||||
optNodeInnerHTML(n) {
|
||||
return n.innerHTML;
|
||||
},
|
||||
optNodeInnerHTML: (n) => n.innerHTML,
|
||||
};
|
||||
|
||||
const _blockElems = {
|
||||
|
@ -77,60 +63,48 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas
|
|||
p: 1,
|
||||
pre: 1,
|
||||
li: 1,
|
||||
img: 1,
|
||||
};
|
||||
|
||||
_.each(hooks.callAll('ccRegisterBlockElements'), (element) => {
|
||||
_blockElems[element] = 1;
|
||||
});
|
||||
|
||||
function isBlockElement(n) {
|
||||
return !!_blockElems[(dom.nodeTagName(n) || '').toLowerCase()];
|
||||
}
|
||||
const isBlockElement = (n) => !!_blockElems[(dom.nodeTagName(n) || '').toLowerCase()];
|
||||
|
||||
function textify(str) {
|
||||
return sanitizeUnicode(
|
||||
str.replace(/(\n | \n)/g, ' ').replace(/[\n\r ]/g, ' ').replace(/\xa0/g, ' ').replace(/\t/g, ' '));
|
||||
}
|
||||
const textify = (str) => sanitizeUnicode(
|
||||
str.replace(/(\n | \n)/g, ' ')
|
||||
.replace(/[\n\r ]/g, ' ')
|
||||
.replace(/\xa0/g, ' ')
|
||||
.replace(/\t/g, ' '));
|
||||
|
||||
function getAssoc(node, name) {
|
||||
return dom.nodeProp(node, `_magicdom_${name}`);
|
||||
}
|
||||
const getAssoc = (node, name) => dom.nodeProp(node, `_magicdom_${name}`);
|
||||
|
||||
const lines = (function () {
|
||||
const lines = (() => {
|
||||
const textArray = [];
|
||||
const attribsArray = [];
|
||||
let attribsBuilder = null;
|
||||
const op = Changeset.newOp('+');
|
||||
var self = {
|
||||
length() {
|
||||
return textArray.length;
|
||||
},
|
||||
atColumnZero() {
|
||||
return textArray[textArray.length - 1] === '';
|
||||
},
|
||||
startNew() {
|
||||
const self = {
|
||||
length: () => textArray.length,
|
||||
atColumnZero: () => textArray[textArray.length - 1] === '',
|
||||
startNew: () => {
|
||||
textArray.push('');
|
||||
self.flush(true);
|
||||
attribsBuilder = Changeset.smartOpAssembler();
|
||||
},
|
||||
textOfLine(i) {
|
||||
return textArray[i];
|
||||
},
|
||||
appendText(txt, attrString) {
|
||||
textOfLine: (i) => textArray[i],
|
||||
appendText: (txt, attrString) => {
|
||||
textArray[textArray.length - 1] += txt;
|
||||
// dmesg(txt+" / "+attrString);
|
||||
op.attribs = attrString;
|
||||
op.chars = txt.length;
|
||||
attribsBuilder.append(op);
|
||||
},
|
||||
textLines() {
|
||||
return textArray.slice();
|
||||
},
|
||||
attribLines() {
|
||||
return attribsArray;
|
||||
},
|
||||
textLines: () => textArray.slice(),
|
||||
attribLines: () => attribsArray,
|
||||
// call flush only when you're done
|
||||
flush(withNewline) {
|
||||
flush: (withNewline) => {
|
||||
if (attribsBuilder) {
|
||||
attribsArray.push(attribsBuilder.toString());
|
||||
attribsBuilder = null;
|
||||
|
@ -139,21 +113,24 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas
|
|||
};
|
||||
self.startNew();
|
||||
return self;
|
||||
}());
|
||||
})();
|
||||
const cc = {};
|
||||
|
||||
function _ensureColumnZero(state) {
|
||||
const _ensureColumnZero = (state) => {
|
||||
if (!lines.atColumnZero()) {
|
||||
cc.startNewLine(state);
|
||||
}
|
||||
}
|
||||
};
|
||||
let selection, startPoint, endPoint;
|
||||
let selStart = [-1, -1];
|
||||
let selEnd = [-1, -1];
|
||||
function _isEmpty(node, state) {
|
||||
const _isEmpty = (node, state) => {
|
||||
// consider clean blank lines pasted in IE to be empty
|
||||
if (dom.nodeNumChildren(node) == 0) return true;
|
||||
if (dom.nodeNumChildren(node) == 1 && getAssoc(node, 'shouldBeEmpty') && dom.optNodeInnerHTML(node) == ' ' && !getAssoc(node, 'unpasted')) {
|
||||
if (dom.nodeNumChildren(node) === 0) return true;
|
||||
if (dom.nodeNumChildren(node) === 1 &&
|
||||
getAssoc(node, 'shouldBeEmpty') &&
|
||||
dom.optNodeInnerHTML(node) === ' ' &&
|
||||
!getAssoc(node, 'unpasted')) {
|
||||
if (state) {
|
||||
const child = dom.nodeChild(node, 0);
|
||||
_reachPoint(child, 0, state);
|
||||
|
@ -162,37 +139,37 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas
|
|||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
function _pointHere(charsAfter, state) {
|
||||
const _pointHere = (charsAfter, state) => {
|
||||
const ln = lines.length() - 1;
|
||||
let chr = lines.textOfLine(ln).length;
|
||||
if (chr == 0 && !_.isEmpty(state.lineAttributes)) {
|
||||
if (chr === 0 && !_.isEmpty(state.lineAttributes)) {
|
||||
chr += 1; // listMarker
|
||||
}
|
||||
chr += charsAfter;
|
||||
return [ln, chr];
|
||||
}
|
||||
};
|
||||
|
||||
function _reachBlockPoint(nd, idx, state) {
|
||||
const _reachBlockPoint = (nd, idx, state) => {
|
||||
if (!dom.isNodeText(nd)) _reachPoint(nd, idx, state);
|
||||
}
|
||||
};
|
||||
|
||||
function _reachPoint(nd, idx, state) {
|
||||
if (startPoint && nd == startPoint.node && startPoint.index == idx) {
|
||||
const _reachPoint = (nd, idx, state) => {
|
||||
if (startPoint && nd === startPoint.node && startPoint.index === idx) {
|
||||
selStart = _pointHere(0, state);
|
||||
}
|
||||
if (endPoint && nd == endPoint.node && endPoint.index == idx) {
|
||||
if (endPoint && nd === endPoint.node && endPoint.index === idx) {
|
||||
selEnd = _pointHere(0, state);
|
||||
}
|
||||
}
|
||||
cc.incrementFlag = function (state, flagName) {
|
||||
};
|
||||
cc.incrementFlag = (state, flagName) => {
|
||||
state.flags[flagName] = (state.flags[flagName] || 0) + 1;
|
||||
};
|
||||
cc.decrementFlag = function (state, flagName) {
|
||||
cc.decrementFlag = (state, flagName) => {
|
||||
state.flags[flagName]--;
|
||||
};
|
||||
cc.incrementAttrib = function (state, attribName) {
|
||||
cc.incrementAttrib = (state, attribName) => {
|
||||
if (!state.attribs[attribName]) {
|
||||
state.attribs[attribName] = 1;
|
||||
} else {
|
||||
|
@ -200,15 +177,15 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas
|
|||
}
|
||||
_recalcAttribString(state);
|
||||
};
|
||||
cc.decrementAttrib = function (state, attribName) {
|
||||
cc.decrementAttrib = (state, attribName) => {
|
||||
state.attribs[attribName]--;
|
||||
_recalcAttribString(state);
|
||||
};
|
||||
|
||||
function _enterList(state, listType) {
|
||||
const _enterList = (state, listType) => {
|
||||
if (!listType) return;
|
||||
const oldListType = state.lineAttributes.list;
|
||||
if (listType != 'none') {
|
||||
if (listType !== 'none') {
|
||||
state.listNesting = (state.listNesting || 0) + 1;
|
||||
// reminder that listType can be "number2", "number3" etc.
|
||||
if (listType.indexOf('number') !== -1) {
|
||||
|
@ -223,36 +200,36 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas
|
|||
}
|
||||
_recalcAttribString(state);
|
||||
return oldListType;
|
||||
}
|
||||
};
|
||||
|
||||
function _exitList(state, oldListType) {
|
||||
const _exitList = (state, oldListType) => {
|
||||
if (state.lineAttributes.list) {
|
||||
state.listNesting--;
|
||||
}
|
||||
if (oldListType && oldListType != 'none') {
|
||||
if (oldListType && oldListType !== 'none') {
|
||||
state.lineAttributes.list = oldListType;
|
||||
} else {
|
||||
delete state.lineAttributes.list;
|
||||
delete state.lineAttributes.start;
|
||||
}
|
||||
_recalcAttribString(state);
|
||||
}
|
||||
};
|
||||
|
||||
function _enterAuthor(state, author) {
|
||||
const _enterAuthor = (state, author) => {
|
||||
const oldAuthor = state.author;
|
||||
state.authorLevel = (state.authorLevel || 0) + 1;
|
||||
state.author = author;
|
||||
_recalcAttribString(state);
|
||||
return oldAuthor;
|
||||
}
|
||||
};
|
||||
|
||||
function _exitAuthor(state, oldAuthor) {
|
||||
const _exitAuthor = (state, oldAuthor) => {
|
||||
state.authorLevel--;
|
||||
state.author = oldAuthor;
|
||||
_recalcAttribString(state);
|
||||
}
|
||||
};
|
||||
|
||||
function _recalcAttribString(state) {
|
||||
const _recalcAttribString = (state) => {
|
||||
const lst = [];
|
||||
for (const a in state.attribs) {
|
||||
if (state.attribs[a]) {
|
||||
|
@ -284,9 +261,9 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas
|
|||
}
|
||||
}
|
||||
state.attribString = Changeset.makeAttribsString('+', lst, apool);
|
||||
}
|
||||
};
|
||||
|
||||
function _produceLineAttributesMarker(state) {
|
||||
const _produceLineAttributesMarker = (state) => {
|
||||
// TODO: This has to go to AttributeManager.
|
||||
const attributes = [
|
||||
['lmkr', '1'],
|
||||
|
@ -295,24 +272,24 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas
|
|||
_.map(state.lineAttributes, (value, key) => [key, value])
|
||||
);
|
||||
lines.appendText('*', Changeset.makeAttribsString('+', attributes, apool));
|
||||
}
|
||||
cc.startNewLine = function (state) {
|
||||
};
|
||||
cc.startNewLine = (state) => {
|
||||
if (state) {
|
||||
const atBeginningOfLine = lines.textOfLine(lines.length() - 1).length == 0;
|
||||
const atBeginningOfLine = lines.textOfLine(lines.length() - 1).length === 0;
|
||||
if (atBeginningOfLine && !_.isEmpty(state.lineAttributes)) {
|
||||
_produceLineAttributesMarker(state);
|
||||
}
|
||||
}
|
||||
lines.startNew();
|
||||
};
|
||||
cc.notifySelection = function (sel) {
|
||||
cc.notifySelection = (sel) => {
|
||||
if (sel) {
|
||||
selection = sel;
|
||||
startPoint = selection.startPoint;
|
||||
endPoint = selection.endPoint;
|
||||
}
|
||||
};
|
||||
cc.doAttrib = function (state, na) {
|
||||
cc.doAttrib = (state, na) => {
|
||||
state.localAttribs = (state.localAttribs || []);
|
||||
state.localAttribs.push(na);
|
||||
cc.incrementAttrib(state, na);
|
||||
|
@ -342,9 +319,10 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas
|
|||
if (isBlock) _ensureColumnZero(state);
|
||||
const startLine = lines.length() - 1;
|
||||
_reachBlockPoint(node, 0, state);
|
||||
|
||||
if (dom.isNodeText(node)) {
|
||||
let txt = dom.nodeValue(node);
|
||||
var tname = dom.nodeAttr(node.parentNode, 'name');
|
||||
const tname = dom.nodeAttr(node.parentNode, 'name');
|
||||
|
||||
const txtFromHook = hooks.callAll('collectContentLineText', {
|
||||
cc: this,
|
||||
|
@ -364,11 +342,11 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas
|
|||
|
||||
let rest = '';
|
||||
let x = 0; // offset into original text
|
||||
if (txt.length == 0) {
|
||||
if (startPoint && node == startPoint.node) {
|
||||
if (txt.length === 0) {
|
||||
if (startPoint && node === startPoint.node) {
|
||||
selStart = _pointHere(0, state);
|
||||
}
|
||||
if (endPoint && node == endPoint.node) {
|
||||
if (endPoint && node === endPoint.node) {
|
||||
selEnd = _pointHere(0, state);
|
||||
}
|
||||
}
|
||||
|
@ -381,10 +359,10 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas
|
|||
txt = firstLine;
|
||||
} else { /* will only run this loop body once */
|
||||
}
|
||||
if (startPoint && node == startPoint.node && startPoint.index - x <= txt.length) {
|
||||
if (startPoint && node === startPoint.node && startPoint.index - x <= txt.length) {
|
||||
selStart = _pointHere(startPoint.index - x, state);
|
||||
}
|
||||
if (endPoint && node == endPoint.node && endPoint.index - x <= txt.length) {
|
||||
if (endPoint && node === endPoint.node && endPoint.index - x <= txt.length) {
|
||||
selEnd = _pointHere(endPoint.index - x, state);
|
||||
}
|
||||
let txt2 = txt;
|
||||
|
@ -395,7 +373,7 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas
|
|||
// removing "\n" from pasted HTML will collapse words together.
|
||||
txt2 = '';
|
||||
}
|
||||
const atBeginningOfLine = lines.textOfLine(lines.length() - 1).length == 0;
|
||||
const atBeginningOfLine = lines.textOfLine(lines.length() - 1).length === 0;
|
||||
if (atBeginningOfLine) {
|
||||
// newlines in the source mustn't become spaces at beginning of line box
|
||||
txt2 = txt2.replace(/^\n*/, '');
|
||||
|
@ -411,10 +389,20 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas
|
|||
}
|
||||
}
|
||||
} else {
|
||||
var tname = (dom.nodeTagName(node) || '').toLowerCase();
|
||||
// Not a text node..
|
||||
const tname = (dom.nodeTagName(node) || '').toLowerCase();
|
||||
const styl = dom.nodeAttr(node, 'style');
|
||||
const cls = dom.nodeAttr(node, 'class');
|
||||
|
||||
if (tname == 'img') {
|
||||
const collectContentImage = hooks.callAll('collectContentImage', {
|
||||
// clear to avoid pollution of trailing blank lines after image lines
|
||||
// with attributes during import
|
||||
if (state.lineAttributes && state.lineAttributes.img) {
|
||||
delete state.lineAttributes.img;
|
||||
}
|
||||
|
||||
if (tname === 'img') {
|
||||
// no hook sare called in this branch, just a demo..
|
||||
hooks.callAll('collectContentImage', {
|
||||
cc,
|
||||
state,
|
||||
tname,
|
||||
|
@ -422,12 +410,15 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas
|
|||
cls,
|
||||
node,
|
||||
});
|
||||
} else {
|
||||
// THIS SEEMS VERY HACKY! -- Please submit a better fix!
|
||||
delete state.lineAttributes.img;
|
||||
}
|
||||
|
||||
if (tname == 'br') {
|
||||
// Client side processing
|
||||
if (node.src) state.lineAttributes.img = node.src;
|
||||
// Server side processing
|
||||
if (node.attribs && node.attribs.src) state.lineAttributes.img = node.attribs.src;
|
||||
} else if (tname === 'br') {
|
||||
// Delete line Attributes that can pollute line breaks, they should only
|
||||
// be present in the line itself, not in any attributes of a line..
|
||||
// uncommenting the below will make duplicate images.. :)
|
||||
if (state.lineAttributes) delete state.lineAttributes;
|
||||
this.breakLine = true;
|
||||
const tvalue = dom.nodeAttr(node, 'value');
|
||||
const induceLineBreak = hooks.callAll('collectContentLineBreak', {
|
||||
|
@ -438,17 +429,19 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas
|
|||
styl: null,
|
||||
cls: null,
|
||||
});
|
||||
const startNewLine = (typeof (induceLineBreak) === 'object' && induceLineBreak.length == 0) ? true : induceLineBreak[0];
|
||||
const startNewLine = (
|
||||
typeof (induceLineBreak) === 'object' &&
|
||||
induceLineBreak.length === 0) ? true : induceLineBreak[0];
|
||||
if (startNewLine) {
|
||||
cc.startNewLine(state);
|
||||
}
|
||||
} else if (tname == 'script' || tname == 'style') {
|
||||
} else if (tname === 'script' || tname === 'style') {
|
||||
// ignore
|
||||
} else if (!isEmpty) {
|
||||
var styl = dom.nodeAttr(node, 'style');
|
||||
var cls = dom.nodeAttr(node, 'class');
|
||||
let isPre = (tname == 'pre');
|
||||
if ((!isPre) && abrowser.safari) {
|
||||
let styl = dom.nodeAttr(node, 'style');
|
||||
let cls = dom.nodeAttr(node, 'class');
|
||||
let isPre = (tname === 'pre');
|
||||
if ((!isPre) && abrowser && abrowser.safari) {
|
||||
isPre = (styl && /\bwhite-space:\s*pre\b/i.exec(styl));
|
||||
}
|
||||
if (isPre) cc.incrementFlag(state, 'preMode');
|
||||
|
@ -460,10 +453,10 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas
|
|||
styl = null;
|
||||
cls = null;
|
||||
|
||||
// We have to return here but this could break things in the future, for now it shows how to fix the problem
|
||||
// We have to return here but this could break things in the future,
|
||||
// for now it shows how to fix the problem
|
||||
return;
|
||||
}
|
||||
|
||||
if (collectStyles) {
|
||||
hooks.callAll('collectContentPre', {
|
||||
cc,
|
||||
|
@ -472,29 +465,39 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas
|
|||
styl,
|
||||
cls,
|
||||
});
|
||||
if (tname == 'b' || (styl && /\bfont-weight:\s*bold\b/i.exec(styl)) || tname == 'strong') {
|
||||
if (tname === 'b' ||
|
||||
(styl && /\bfont-weight:\s*bold\b/i.exec(styl)) ||
|
||||
tname === 'strong') {
|
||||
cc.doAttrib(state, 'bold');
|
||||
}
|
||||
if (tname == 'i' || (styl && /\bfont-style:\s*italic\b/i.exec(styl)) || tname == 'em') {
|
||||
if (tname === 'i' ||
|
||||
(styl && /\bfont-style:\s*italic\b/i.exec(styl)) ||
|
||||
tname === 'em') {
|
||||
cc.doAttrib(state, 'italic');
|
||||
}
|
||||
if (tname == 'u' || (styl && /\btext-decoration:\s*underline\b/i.exec(styl)) || tname == 'ins') {
|
||||
if (tname === 'u' ||
|
||||
(styl && /\btext-decoration:\s*underline\b/i.exec(styl)) ||
|
||||
tname === 'ins') {
|
||||
cc.doAttrib(state, 'underline');
|
||||
}
|
||||
if (tname == 's' || (styl && /\btext-decoration:\s*line-through\b/i.exec(styl)) || tname == 'del') {
|
||||
if (tname === 's' ||
|
||||
(styl && /\btext-decoration:\s*line-through\b/i.exec(styl)) ||
|
||||
tname === 'del') {
|
||||
cc.doAttrib(state, 'strikethrough');
|
||||
}
|
||||
if (tname == 'ul' || tname == 'ol') {
|
||||
let type;
|
||||
if (tname === 'ul' || tname === 'ol') {
|
||||
if (node.attribs) {
|
||||
var type = node.attribs.class;
|
||||
type = node.attribs.class;
|
||||
} else {
|
||||
var type = null;
|
||||
type = null;
|
||||
}
|
||||
const rr = cls && /(?:^| )list-([a-z]+[0-9]+)\b/.exec(cls);
|
||||
// lists do not need to have a type, so before we make a wrong guess, check if we find a better hint within the node's children
|
||||
// lists do not need to have a type, so before we make a wrong guess
|
||||
// check if we find a better hint within the node's children
|
||||
if (!rr && !type) {
|
||||
for (var i in node.children) {
|
||||
if (node.children[i] && node.children[i].name == 'ul') {
|
||||
for (const i in node.children) {
|
||||
if (node.children[i] && node.children[i].name === 'ul') {
|
||||
type = node.children[i].attribs.class;
|
||||
if (type) {
|
||||
break;
|
||||
|
@ -505,8 +508,14 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas
|
|||
if (rr && rr[1]) {
|
||||
type = rr[1];
|
||||
} else {
|
||||
if (tname == 'ul') {
|
||||
if ((type && type.match('indent')) || (node.attribs && node.attribs.class && node.attribs.class.match('indent'))) {
|
||||
if (tname === 'ul') {
|
||||
if ((type && type.match('indent')) ||
|
||||
(
|
||||
node.attribs &&
|
||||
node.attribs.class &&
|
||||
node.attribs.class.match('indent')
|
||||
)
|
||||
) {
|
||||
type = 'indent';
|
||||
} else {
|
||||
type = 'bullet';
|
||||
|
@ -517,7 +526,7 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas
|
|||
type += String(Math.min(_MAX_LIST_LEVEL, (state.listNesting || 0) + 1));
|
||||
}
|
||||
oldListTypeOrNull = (_enterList(state, type) || 'none');
|
||||
} else if ((tname == 'div' || tname == 'p') && cls && cls.match(/(?:^| )ace-line\b/)) {
|
||||
} else if ((tname === 'div' || tname === 'p') && cls && cls.match(/(?:^| )ace-line\b/)) {
|
||||
// This has undesirable behavior in Chrome but is right in other browsers.
|
||||
// See https://github.com/ether/etherpad-lite/issues/2412 for reasoning
|
||||
if (!abrowser.chrome) oldListTypeOrNull = (_enterList(state, type) || 'none');
|
||||
|
@ -565,8 +574,8 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas
|
|||
if (className2Author && cls) {
|
||||
const classes = cls.match(/\S+/g);
|
||||
if (classes && classes.length > 0) {
|
||||
for (var i = 0; i < classes.length; i++) {
|
||||
var c = classes[i];
|
||||
for (let i = 0; i < classes.length; i++) {
|
||||
const c = classes[i];
|
||||
const a = className2Author(c);
|
||||
if (a) {
|
||||
oldAuthorOrNull = (_enterAuthor(state, a) || 'none');
|
||||
|
@ -578,8 +587,8 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas
|
|||
}
|
||||
|
||||
const nc = dom.nodeNumChildren(node);
|
||||
for (var i = 0; i < nc; i++) {
|
||||
var c = dom.nodeChild(node, i);
|
||||
for (let i = 0; i < nc; i++) {
|
||||
const c = dom.nodeChild(node, i);
|
||||
cc.collectContent(c, state);
|
||||
}
|
||||
|
||||
|
@ -595,7 +604,7 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas
|
|||
|
||||
if (isPre) cc.decrementFlag(state, 'preMode');
|
||||
if (state.localAttribs) {
|
||||
for (var i = 0; i < state.localAttribs.length; i++) {
|
||||
for (let i = 0; i < state.localAttribs.length; i++) {
|
||||
cc.decrementAttrib(state, state.localAttribs[i]);
|
||||
}
|
||||
}
|
||||
|
@ -605,11 +614,12 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas
|
|||
if (oldAuthorOrNull) {
|
||||
_exitAuthor(state, oldAuthorOrNull);
|
||||
}
|
||||
console.warn("state", state.lineAttributes);
|
||||
}
|
||||
}
|
||||
_reachBlockPoint(node, 1, state);
|
||||
if (isBlock) {
|
||||
if (lines.length() - 1 == startLine) {
|
||||
if (lines.length() - 1 === startLine) {
|
||||
// added additional check to resolve https://github.com/JohnMcLear/ep_copy_paste_images/issues/20
|
||||
// this does mean that images etc can't be pasted on lists but imho that's fine
|
||||
|
||||
|
@ -626,7 +636,7 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas
|
|||
state.localAttribs = localAttribs;
|
||||
};
|
||||
// can pass a falsy value for end of doc
|
||||
cc.notifyNextNode = function (node) {
|
||||
cc.notifyNextNode = (node) => {
|
||||
// an "empty block" won't end a line; this addresses an issue in IE with
|
||||
// typing into a blank line at the end of the document. typed text
|
||||
// goes into the body, and the empty line div still looks clean.
|
||||
|
@ -637,21 +647,15 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas
|
|||
}
|
||||
};
|
||||
// each returns [line, char] or [-1,-1]
|
||||
const getSelectionStart = function () {
|
||||
return selStart;
|
||||
};
|
||||
const getSelectionEnd = function () {
|
||||
return selEnd;
|
||||
};
|
||||
const getSelectionStart = () => selStart;
|
||||
const getSelectionEnd = () => selEnd;
|
||||
|
||||
// returns array of strings for lines found, last entry will be "" if
|
||||
// last line is complete (i.e. if a following span should be on a new line).
|
||||
// can be called at any point
|
||||
cc.getLines = function () {
|
||||
return lines.textLines();
|
||||
};
|
||||
cc.getLines = () => lines.textLines();
|
||||
|
||||
cc.finish = function () {
|
||||
cc.finish = () => {
|
||||
lines.flush();
|
||||
const lineAttribs = lines.attribLines();
|
||||
const lineStrings = cc.getLines();
|
||||
|
@ -662,17 +666,17 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas
|
|||
const ss = getSelectionStart();
|
||||
const se = getSelectionEnd();
|
||||
|
||||
function fixLongLines() {
|
||||
const fixLongLines = () => {
|
||||
// design mode does not deal with with really long lines!
|
||||
const lineLimit = 2000; // chars
|
||||
const buffer = 10; // chars allowed over before wrapping
|
||||
let linesWrapped = 0;
|
||||
let numLinesAfter = 0;
|
||||
for (var i = lineStrings.length - 1; i >= 0; i--) {
|
||||
for (let i = lineStrings.length - 1; i >= 0; i--) {
|
||||
let oldString = lineStrings[i];
|
||||
let oldAttribString = lineAttribs[i];
|
||||
if (oldString.length > lineLimit + buffer) {
|
||||
var newStrings = [];
|
||||
const newStrings = [];
|
||||
const newAttribStrings = [];
|
||||
while (oldString.length > lineLimit) {
|
||||
// var semiloc = oldString.lastIndexOf(';', lineLimit-1);
|
||||
|
@ -688,13 +692,13 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas
|
|||
newAttribStrings.push(oldAttribString);
|
||||
}
|
||||
|
||||
function fixLineNumber(lineChar) {
|
||||
const fixLineNumber = (lineChar) => {
|
||||
if (lineChar[0] < 0) return;
|
||||
let n = lineChar[0];
|
||||
let c = lineChar[1];
|
||||
if (n > i) {
|
||||
n += (newStrings.length - 1);
|
||||
} else if (n == i) {
|
||||
} else if (n === i) {
|
||||
let a = 0;
|
||||
while (c > newStrings[a].length) {
|
||||
c -= newStrings[a].length;
|
||||
|
@ -704,13 +708,14 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas
|
|||
}
|
||||
lineChar[0] = n;
|
||||
lineChar[1] = c;
|
||||
}
|
||||
};
|
||||
fixLineNumber(ss);
|
||||
fixLineNumber(se);
|
||||
linesWrapped++;
|
||||
numLinesAfter += newStrings.length;
|
||||
|
||||
newStrings.unshift(i, 1);
|
||||
// Still to fix linting issue below.
|
||||
lineStrings.splice.apply(lineStrings, newStrings);
|
||||
newAttribStrings.unshift(i, 1);
|
||||
lineAttribs.splice.apply(lineAttribs, newAttribStrings);
|
||||
|
@ -720,7 +725,7 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas
|
|||
linesWrapped,
|
||||
numLinesAfter,
|
||||
};
|
||||
}
|
||||
};
|
||||
const wrapData = fixLongLines();
|
||||
|
||||
return {
|
||||
|
@ -734,7 +739,7 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas
|
|||
};
|
||||
|
||||
return cc;
|
||||
}
|
||||
};
|
||||
|
||||
exports.sanitizeUnicode = sanitizeUnicode;
|
||||
exports.makeContentCollector = makeContentCollector;
|
||||
|
|
|
@ -26,17 +26,17 @@ const Security = require('./security');
|
|||
const hooks = require('./pluginfw/hooks');
|
||||
const _ = require('./underscore');
|
||||
const lineAttributeMarker = require('./linestylefilter').lineAttributeMarker;
|
||||
const noop = function () {};
|
||||
const noop = () => {};
|
||||
|
||||
|
||||
const domline = {};
|
||||
|
||||
domline.addToLineClass = function (lineClass, cls) {
|
||||
domline.addToLineClass = (lineClass, cls) => {
|
||||
// an "empty span" at any point can be used to add classes to
|
||||
// the line, using line:className. otherwise, we ignore
|
||||
// the span.
|
||||
cls.replace(/\S+/g, (c) => {
|
||||
if (c.indexOf('line:') == 0) {
|
||||
if (c.indexOf('line:') === 0) {
|
||||
// add class to line
|
||||
lineClass = (lineClass ? `${lineClass} ` : '') + c.substring(5);
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ domline.addToLineClass = function (lineClass, cls) {
|
|||
|
||||
// if "document" is falsy we don't create a DOM node, just
|
||||
// an object with innerHTML and className
|
||||
domline.createDomLine = function (nonEmpty, doesWrap, optBrowser, optDocument) {
|
||||
domline.createDomLine = (nonEmpty, doesWrap, optBrowser, optDocument) => {
|
||||
const result = {
|
||||
node: null,
|
||||
appendSpan: noop,
|
||||
|
@ -73,20 +73,19 @@ domline.createDomLine = function (nonEmpty, doesWrap, optBrowser, optDocument) {
|
|||
let postHtml = '';
|
||||
let curHTML = null;
|
||||
|
||||
function processSpaces(s) {
|
||||
return domline.processSpaces(s, doesWrap);
|
||||
}
|
||||
|
||||
const processSpaces = (s) => domline.processSpaces(s, doesWrap);
|
||||
const perTextNodeProcess = (doesWrap ? _.identity : processSpaces);
|
||||
const perHtmlLineProcess = (doesWrap ? processSpaces : _.identity);
|
||||
let lineClass = 'ace-line';
|
||||
|
||||
result.appendSpan = function (txt, cls) {
|
||||
result.appendSpan = (txt, cls) => {
|
||||
// console.log("domline", domline, "txt", txt, "cls", cls)
|
||||
let processedMarker = false;
|
||||
// Handle lineAttributeMarker, if present
|
||||
if (cls.indexOf(lineAttributeMarker) >= 0) {
|
||||
let listType = /(?:^| )list:(\S+)/.exec(cls);
|
||||
const start = /(?:^| )start:(\S+)/.exec(cls);
|
||||
const img = /(?:^| )img:(\S+)/.exec(cls);
|
||||
|
||||
_.map(hooks.callAll('aceDomLinePreProcessLineAttributes', {
|
||||
domline,
|
||||
|
@ -97,6 +96,11 @@ domline.createDomLine = function (nonEmpty, doesWrap, optBrowser, optDocument) {
|
|||
processedMarker |= modifier.processedMarker;
|
||||
});
|
||||
|
||||
if (img) {
|
||||
preHtml += `<img src="${img[1]}">`;
|
||||
processedMarker = true;
|
||||
}
|
||||
|
||||
if (listType) {
|
||||
listType = listType[1];
|
||||
if (listType) {
|
||||
|
@ -105,12 +109,15 @@ domline.createDomLine = function (nonEmpty, doesWrap, optBrowser, optDocument) {
|
|||
postHtml = `</li></ul>${postHtml}`;
|
||||
} else {
|
||||
if (start) { // is it a start of a list with more than one item in?
|
||||
if (start[1] == 1) { // if its the first one at this level?
|
||||
lineClass = `${lineClass} ` + `list-start-${listType}`; // Add start class to DIV node
|
||||
if (start[1] === 1) { // if its the first one at this level?
|
||||
// Add start class to DIV node
|
||||
lineClass = `${lineClass} ` + `list-start-${listType}`;
|
||||
}
|
||||
preHtml += `<ol start=${start[1]} class="list-${Security.escapeHTMLAttribute(listType)}"><li>`;
|
||||
preHtml +=
|
||||
`<ol start=${start[1]} class="list-${Security.escapeHTMLAttribute(listType)}"><li>`;
|
||||
} else {
|
||||
preHtml += `<ol class="list-${Security.escapeHTMLAttribute(listType)}"><li>`; // Handles pasted contents into existing lists
|
||||
// Handles pasted contents into existing lists
|
||||
preHtml += `<ol class="list-${Security.escapeHTMLAttribute(listType)}"><li>`;
|
||||
}
|
||||
postHtml += '</li></ol>';
|
||||
}
|
||||
|
@ -163,18 +170,20 @@ domline.createDomLine = function (nonEmpty, doesWrap, optBrowser, optDocument) {
|
|||
} else if (txt) {
|
||||
if (href) {
|
||||
const urn_schemes = new RegExp('^(about|geo|mailto|tel):');
|
||||
if (!~href.indexOf('://') && !urn_schemes.test(href)) // if the url doesn't include a protocol prefix, assume http
|
||||
{
|
||||
// if the url doesn't include a protocol prefix, assume http
|
||||
if (!~href.indexOf('://') && !urn_schemes.test(href)) {
|
||||
href = `http://${href}`;
|
||||
}
|
||||
// Using rel="noreferrer" stops leaking the URL/location of the pad when clicking links in the document.
|
||||
// Using rel="noreferrer" stops leaking the URL/location of the pad when
|
||||
// clicking links in the document.
|
||||
// Not all browsers understand this attribute, but it's part of the HTML5 standard.
|
||||
// https://html.spec.whatwg.org/multipage/links.html#link-type-noreferrer
|
||||
// Additionally, we do rel="noopener" to ensure a higher level of referrer security.
|
||||
// https://html.spec.whatwg.org/multipage/links.html#link-type-noopener
|
||||
// https://mathiasbynens.github.io/rel-noopener/
|
||||
// https://github.com/ether/etherpad-lite/pull/3636
|
||||
extraOpenTags = `${extraOpenTags}<a href="${Security.escapeHTMLAttribute(href)}" rel="noreferrer noopener">`;
|
||||
const escapedHref = Security.escapeHTMLAttribute(href);
|
||||
extraOpenTags = `${extraOpenTags}<a href="${escapedHref}" rel="noreferrer noopener">`;
|
||||
extraCloseTags = `</a>${extraCloseTags}`;
|
||||
}
|
||||
if (simpleTags) {
|
||||
|
@ -183,16 +192,23 @@ domline.createDomLine = function (nonEmpty, doesWrap, optBrowser, optDocument) {
|
|||
simpleTags.reverse();
|
||||
extraCloseTags = `</${simpleTags.join('></')}>${extraCloseTags}`;
|
||||
}
|
||||
html.push('<span class="', Security.escapeHTMLAttribute(cls || ''), '">', extraOpenTags, perTextNodeProcess(Security.escapeHTML(txt)), extraCloseTags, '</span>');
|
||||
html.push('<span class="', Security.escapeHTMLAttribute(cls || ''),
|
||||
'">',
|
||||
extraOpenTags,
|
||||
perTextNodeProcess(
|
||||
Security.escapeHTML(txt)
|
||||
),
|
||||
extraCloseTags,
|
||||
'</span>');
|
||||
}
|
||||
};
|
||||
result.clearSpans = function () {
|
||||
result.clearSpans = () => {
|
||||
html = [];
|
||||
lineClass = 'ace-line';
|
||||
result.lineMarker = 0;
|
||||
};
|
||||
|
||||
function writeHTML() {
|
||||
const writeHTML = () => {
|
||||
let newHTML = perHtmlLineProcess(html.join(''));
|
||||
if (!newHTML) {
|
||||
if ((!document) || (!optBrowser)) {
|
||||
|
@ -209,21 +225,19 @@ domline.createDomLine = function (nonEmpty, doesWrap, optBrowser, optDocument) {
|
|||
curHTML = newHTML;
|
||||
result.node.innerHTML = curHTML;
|
||||
}
|
||||
if (lineClass !== null) result.node.className = lineClass;
|
||||
if (lineClass != null) result.node.className = lineClass;
|
||||
|
||||
hooks.callAll('acePostWriteDomLineHTML', {
|
||||
node: result.node,
|
||||
});
|
||||
}
|
||||
};
|
||||
result.prepareForAdd = writeHTML;
|
||||
result.finishUpdate = writeHTML;
|
||||
result.getInnerHTML = function () {
|
||||
return curHTML || '';
|
||||
};
|
||||
result.getInnerHTML = () => curHTML || '';
|
||||
return result;
|
||||
};
|
||||
|
||||
domline.processSpaces = function (s, doesWrap) {
|
||||
domline.processSpaces = (s, doesWrap) => {
|
||||
if (s.indexOf('<') < 0 && !doesWrap) {
|
||||
// short-cut
|
||||
return s.replace(/ /g, ' ');
|
||||
|
@ -237,31 +251,31 @@ domline.processSpaces = function (s, doesWrap) {
|
|||
let beforeSpace = false;
|
||||
// last space in a run is normal, others are nbsp,
|
||||
// end of line is nbsp
|
||||
for (var i = parts.length - 1; i >= 0; i--) {
|
||||
var p = parts[i];
|
||||
if (p == ' ') {
|
||||
for (let i = parts.length - 1; i >= 0; i--) {
|
||||
const p = parts[i];
|
||||
if (p === ' ') {
|
||||
if (endOfLine || beforeSpace) parts[i] = ' ';
|
||||
endOfLine = false;
|
||||
beforeSpace = true;
|
||||
} else if (p.charAt(0) != '<') {
|
||||
} else if (p.charAt(0) !== '<') {
|
||||
endOfLine = false;
|
||||
beforeSpace = false;
|
||||
}
|
||||
}
|
||||
// beginning of line is nbsp
|
||||
for (var i = 0; i < parts.length; i++) {
|
||||
var p = parts[i];
|
||||
if (p == ' ') {
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const p = parts[i];
|
||||
if (p === ' ') {
|
||||
parts[i] = ' ';
|
||||
break;
|
||||
} else if (p.charAt(0) != '<') {
|
||||
} else if (p.charAt(0) !== '<') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (var i = 0; i < parts.length; i++) {
|
||||
var p = parts[i];
|
||||
if (p == ' ') {
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const p = parts[i];
|
||||
if (p === ' ') {
|
||||
parts[i] = ' ';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -72,7 +72,6 @@ linestylefilter.getLineStyleFilter = (lineLength, aline, textAndClassFunc, apool
|
|||
const attribsToClasses = (attribs) => {
|
||||
let classes = '';
|
||||
let isLineAttribMarker = false;
|
||||
|
||||
// For each attribute number
|
||||
Changeset.eachAttribNumber(attribs, (n) => {
|
||||
// Give us this attributes key
|
||||
|
@ -92,6 +91,8 @@ linestylefilter.getLineStyleFilter = (lineLength, aline, textAndClassFunc, apool
|
|||
classes += ` start:${value}`;
|
||||
} else if (linestylefilter.ATTRIB_CLASSES[key]) {
|
||||
classes += ` ${linestylefilter.ATTRIB_CLASSES[key]}`;
|
||||
} else if (key === 'img') {
|
||||
classes += ` img:${value}`;
|
||||
} else {
|
||||
classes += hooks.callAllStr('aceAttribsToClasses', {
|
||||
linestylefilter,
|
||||
|
|
|
@ -282,7 +282,7 @@ Scroll.prototype.scrollNodeVerticallyIntoView = function (rep, innerHeight) {
|
|||
const linePosition = caretPosition.getPosition();
|
||||
if (linePosition) {
|
||||
const distanceOfTopOfViewport = linePosition.top - viewport.top;
|
||||
const distanceOfBottomOfViewport = viewport.bottom - linePosition.bottom;
|
||||
const distanceOfBottomOfViewport = viewport.bottom - linePosition.bottom - linePosition.height;
|
||||
const caretIsAboveOfViewport = distanceOfTopOfViewport < 0;
|
||||
const caretIsBelowOfViewport = distanceOfBottomOfViewport < 0;
|
||||
if (caretIsAboveOfViewport) {
|
||||
|
@ -290,11 +290,15 @@ Scroll.prototype.scrollNodeVerticallyIntoView = function (rep, innerHeight) {
|
|||
distanceOfTopOfViewport - this._getPixelsRelativeToPercentageOfViewport(innerHeight, true);
|
||||
this._scrollYPage(pixelsToScroll);
|
||||
} else if (caretIsBelowOfViewport) {
|
||||
const pixelsToScroll = -distanceOfBottomOfViewport +
|
||||
this._getPixelsRelativeToPercentageOfViewport(innerHeight);
|
||||
this._scrollYPage(pixelsToScroll);
|
||||
} else {
|
||||
this.scrollWhenCaretIsInTheLastLineOfViewportWhenNecessary(rep, true, innerHeight);
|
||||
// setTimeout is required here as line might not be fully rendered onto the pad
|
||||
setTimeout(() => {
|
||||
const outer = window.parent;
|
||||
// scroll to the very end of the pad outer
|
||||
outer.scrollTo(0, outer[0].innerHeight);
|
||||
}, 150);
|
||||
// if the above setTimeout and functionality is removed then hitting an enter
|
||||
// key while on the last line wont be an optimal user experience
|
||||
// Details at: https://github.com/ether/etherpad-lite/pull/4639/files
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,13 +1,13 @@
|
|||
'use strict';
|
||||
|
||||
describe('enter keystroke', function () {
|
||||
// create a new pad before each test run
|
||||
beforeEach(function (cb) {
|
||||
helper.newPad(cb);
|
||||
this.timeout(60000);
|
||||
});
|
||||
|
||||
it('creates a new line & puts cursor onto a new line', function (done) {
|
||||
const inner$ = helper.padInner$;
|
||||
const chrome$ = helper.padChrome$;
|
||||
|
||||
// get the first text element out of the inner iframe
|
||||
const $firstTextElement = inner$('div').first();
|
||||
|
@ -29,4 +29,32 @@ describe('enter keystroke', function () {
|
|||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('enter is always visible after event', async function () {
|
||||
const originalLength = helper.padInner$('div').length;
|
||||
let $lastLine = helper.padInner$('div').last();
|
||||
|
||||
// simulate key presses to enter content
|
||||
let i = 0;
|
||||
const numberOfLines = 15;
|
||||
let previousLineLength = originalLength;
|
||||
while (i < numberOfLines) {
|
||||
$lastLine = helper.padInner$('div').last();
|
||||
$lastLine.sendkeys('{enter}');
|
||||
await helper.waitFor(() => helper.padInner$('div').length > previousLineLength);
|
||||
previousLineLength = helper.padInner$('div').length;
|
||||
// check we can see the caret..
|
||||
|
||||
i++;
|
||||
}
|
||||
await helper.waitFor(() => helper.padInner$('div').length === numberOfLines + originalLength);
|
||||
|
||||
// is edited line fully visible?
|
||||
const lastLine = helper.padInner$('div').last();
|
||||
const bottomOfLastLine = lastLine.offset().top + lastLine.height();
|
||||
const scrolledWindow = helper.padChrome$('iframe')[0];
|
||||
const scrolledAmount = scrolledWindow.contentWindow.pageYOffset +
|
||||
scrolledWindow.contentWindow.innerHeight;
|
||||
await helper.waitFor(() => scrolledAmount >= bottomOfLastLine);
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue