initial support for auto complete in model and parts

also move part creation out of model, into partcreator, which
can then also contain dependencies for creating the auto completer.
pull/21833/head
Bruno Windels 2019-05-09 14:59:52 +02:00
parent 7507d0d7e1
commit 1330b438d6
2 changed files with 131 additions and 22 deletions

View File

@ -14,22 +14,36 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {PlainPart, RoomPillPart, UserPillPart} from "./parts";
import {diffAtCaret, diffDeletion} from "./diff"; import {diffAtCaret, diffDeletion} from "./diff";
export default class EditorModel { export default class EditorModel {
constructor(parts = []) { constructor(parts, partCreator) {
this._parts = parts; this._parts = parts;
this.actions = null; this._partCreator = partCreator;
this._previousValue = parts.reduce((text, p) => text + p.text, ""); this._previousValue = parts.reduce((text, p) => text + p.text, "");
this._activePartIdx = null;
this._autoComplete = null;
this._autoCompletePartIdx = null;
} }
_insertPart(index, part) { _insertPart(index, part) {
this._parts.splice(index, 0, part); this._parts.splice(index, 0, part);
if (this._activePartIdx >= index) {
++this._activePartIdx;
}
if (this._autoCompletePartIdx >= index) {
++this._autoCompletePartIdx;
}
} }
_removePart(index) { _removePart(index) {
this._parts.splice(index, 1); this._parts.splice(index, 1);
if (this._activePartIdx >= index) {
--this._activePartIdx;
}
if (this._autoCompletePartIdx >= index) {
--this._autoCompletePartIdx;
}
} }
_replacePart(index, part) { _replacePart(index, part) {
@ -40,11 +54,21 @@ export default class EditorModel {
return this._parts; return this._parts;
} }
get autoComplete() {
if (this._activePartIdx === this._autoCompletePartIdx) {
return this._autoComplete;
}
return null;
}
serializeParts() { serializeParts() {
return this._parts.map(({type, text}) => {return {type, text};}); return this._parts.map(({type, text}) => {return {type, text};});
} }
_diff(newValue, inputType, caret) { _diff(newValue, inputType, caret) {
// handle deleteContentForward (Delete key)
// and deleteContentBackward (Backspace)
// can't use caret position with drag and drop // can't use caret position with drag and drop
if (inputType === "deleteByDrag") { if (inputType === "deleteByDrag") {
return diffDeletion(this._previousValue, newValue); return diffDeletion(this._previousValue, newValue);
@ -66,9 +90,38 @@ export default class EditorModel {
this._mergeAdjacentParts(); this._mergeAdjacentParts();
this._previousValue = newValue; this._previousValue = newValue;
const caretOffset = diff.at + (diff.added ? diff.added.length : 0); const caretOffset = diff.at + (diff.added ? diff.added.length : 0);
return this._positionForOffset(caretOffset, true); const newPosition = this._positionForOffset(caretOffset, true);
this._setActivePart(newPosition);
return newPosition;
} }
_setActivePart(pos) {
const {index} = pos;
const part = this._parts[index];
if (pos.index !== this._activePartIdx) {
this._activePartIdx = index;
// if there is a hidden autocomplete for this part, show it again
if (this._activePartIdx !== this._autoCompletePartIdx) {
// else try to create one
const ac = part.createAutoComplete(this._onAutoComplete);
if (ac) {
// make sure that react picks up the difference between both acs
this._autoComplete = ac;
this._autoCompletePartIdx = index;
}
}
}
if (this._autoComplete) {
this._autoComplete.onPartUpdate(part, pos.offset);
}
}
/*
updateCaret(caret) {
// update active part here as well, hiding/showing autocomplete if needed
}
*/
_mergeAdjacentParts(docPos) { _mergeAdjacentParts(docPos) {
let prevPart = this._parts[0]; let prevPart = this._parts[0];
for (let i = 1; i < this._parts.length; ++i) { for (let i = 1; i < this._parts.length; ++i) {
@ -94,7 +147,7 @@ export default class EditorModel {
const amount = Math.min(len, part.text.length - offset); const amount = Math.min(len, part.text.length - offset);
const replaceWith = part.remove(offset, amount); const replaceWith = part.remove(offset, amount);
if (typeof replaceWith === "string") { if (typeof replaceWith === "string") {
this._replacePart(index, new PlainPart(replaceWith)); this._replacePart(index, this._partCreator.createDefaultPart(replaceWith));
} }
part = this._parts[index]; part = this._parts[index];
// remove empty part // remove empty part
@ -123,17 +176,7 @@ export default class EditorModel {
} }
} }
while (str) { while (str) {
let newPart; const newPart = this._partCreator.createPartForInput(str);
switch (str[0]) {
case "#":
newPart = new RoomPillPart();
break;
case "@":
newPart = new UserPillPart();
break;
default:
newPart = new PlainPart();
}
str = newPart.appendUntilRejected(str); str = newPart.appendUntilRejected(str);
this._insertPart(index, newPart); this._insertPart(index, newPart);
index += 1; index += 1;
@ -156,6 +199,14 @@ export default class EditorModel {
return new DocumentPosition(index, totalOffset - currentOffset); return new DocumentPosition(index, totalOffset - currentOffset);
} }
_onAutoComplete = ({replacePart, replaceCaret, close}) => {
this._replacePart(this._autoCompletePartIdx, replacePart);
if (close) {
this._autoComplete = null;
this._autoCompletePartIdx = null;
}
}
} }
class DocumentPosition { class DocumentPosition {

View File

@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import AutocompleteWrapperModel from "./autocomplete";
class BasePart { class BasePart {
constructor(text = "") { constructor(text = "") {
this._text = text; this._text = text;
@ -39,8 +41,6 @@ class BasePart {
// removes len chars, or returns the plain text this part should be replaced with // removes len chars, or returns the plain text this part should be replaced with
// if the part would become invalid if it removed everything. // if the part would become invalid if it removed everything.
// TODO: this should probably return the Part and caret position within this should be replaced with
remove(offset, len) { remove(offset, len) {
// validate // validate
const strWithRemoval = this.text.substr(0, offset) + this.text.substr(offset + len); const strWithRemoval = this.text.substr(0, offset) + this.text.substr(offset + len);
@ -80,6 +80,7 @@ class BasePart {
return true; return true;
} }
createAutoComplete() {}
trim(len) { trim(len) {
const remaining = this._text.substr(len); const remaining = this._text.substr(len);
@ -94,7 +95,7 @@ class BasePart {
export class PlainPart extends BasePart { export class PlainPart extends BasePart {
acceptsInsertion(chr) { acceptsInsertion(chr) {
return chr !== "@" && chr !== "#"; return chr !== "@" && chr !== "#" && chr !== ":";
} }
toDOMNode() { toDOMNode() {
@ -126,6 +127,11 @@ export class PlainPart extends BasePart {
} }
class PillPart extends BasePart { class PillPart extends BasePart {
constructor(resourceId, label) {
super(label);
this.resourceId = resourceId;
}
acceptsInsertion(chr) { acceptsInsertion(chr) {
return chr !== " "; return chr !== " ";
} }
@ -162,6 +168,10 @@ class PillPart extends BasePart {
} }
export class RoomPillPart extends PillPart { export class RoomPillPart extends PillPart {
constructor(displayAlias) {
super(displayAlias, displayAlias);
}
get type() { get type() {
return "room-pill"; return "room-pill";
} }
@ -172,3 +182,51 @@ export class UserPillPart extends PillPart {
return "user-pill"; return "user-pill";
} }
} }
export class PillCandidatePart extends PlainPart {
constructor(text, autoCompleteCreator) {
super(text);
this._autoCompleteCreator = autoCompleteCreator;
}
createAutoComplete(updateCallback) {
return this._autoCompleteCreator(updateCallback);
}
acceptsInsertion(chr) {
return true;
}
acceptsRemoval(position, chr) {
return true;
}
get type() {
return "pill-candidate";
}
}
export class PartCreator {
constructor(getAutocompleterComponent, updateQuery) {
this._autoCompleteCreator = (updateCallback) => {
return new AutocompleteWrapperModel(updateCallback, getAutocompleterComponent, updateQuery);
};
}
createPartForInput(input) {
switch (input[0]) {
case "#":
case "@":
case ":":
return new PillCandidatePart("", this._autoCompleteCreator);
default:
return new PlainPart();
}
}
createDefaultPart(text) {
return new PlainPart(text);
}
}