Compare commits

...

17 Commits

Author SHA1 Message Date
John McLear d9ca1efd4f working 2021-01-14 17:02:43 +00:00
John McLear b876e2dfe0 working? 2021-01-14 16:55:04 +00:00
John McLear 1cad0a6707 server side processing too 2021-01-14 16:23:27 +00:00
John McLear 1cbaf2d272 dont expect safari 2021-01-14 14:08:53 +00:00
John McLear c6b0c92061 stable :) 2021-01-14 14:03:29 +00:00
John McLear f466bcd32f lint: domline 2021-01-14 13:20:29 +00:00
John McLear 5bedfa0ba1 Merge branch 'develop' of github.com:ether/etherpad-lite into image-example 2021-01-14 13:07:20 +00:00
John McLear 66109002ef placeholder commit 2021-01-14 12:58:06 +00:00
John McLear 638bd9942e lint: contentcollector 2021-01-14 11:24:59 +00:00
John McLear 183b765df0 how did it conflict on that? 2021-01-14 10:25:34 +00:00
John McLear e39e959347 basic test and hacked in stuff 2021-01-12 20:21:55 +00:00
John McLear 62906d72a2 tidy up 2021-01-10 15:51:02 +00:00
John McLear f5cfb8b135 tidy up 2021-01-10 15:50:01 +00:00
John McLear f0a9e6832f include proposed fix 2021-01-10 15:47:42 +00:00
John McLear 1f667f86ef ah so a set timeout is required? 2021-01-10 11:55:46 +00:00
John McLear ef2a929016 test coverage 2021-01-09 19:48:15 +00:00
John McLear b26548011c editor: fix enter key keep line in view 2021-01-08 14:22:26 +00:00
6 changed files with 261 additions and 203 deletions

View File

@ -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;

View File

@ -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, '&nbsp;');
@ -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] = '&nbsp;';
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] = '&nbsp;';
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] = '&nbsp;';
}
}

View File

@ -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,

View File

@ -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

View File

@ -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);
});
});