@atlaskit/editor-core
Version:
A package contains Atlassian editor core functionality
334 lines • 14.5 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var prosemirror_1 = require("../../prosemirror");
var commands = require("../../commands");
var input_rule_1 = require("./input-rule");
var keymap_1 = require("./keymap");
var utils_1 = require("./utils");
var plugin_key_1 = require("./plugin-key");
exports.stateKey = plugin_key_1.default;
var HyperlinkState = (function () {
function HyperlinkState(state) {
this.active = false;
this.linkable = false;
this.editorFocused = false;
this.showToolbarPanel = false;
this.changeHandlers = [];
this.changeHandlers = [];
}
HyperlinkState.prototype.subscribe = function (cb) {
this.changeHandlers.push(cb);
cb(this);
};
HyperlinkState.prototype.unsubscribe = function (cb) {
this.changeHandlers = this.changeHandlers.filter(function (ch) { return ch !== cb; });
};
HyperlinkState.prototype.addLink = function (options, view) {
if (this.linkable && !this.active) {
var state = this.state;
var href = options.href, text = options.text;
var _a = state.selection, empty = _a.empty, $from = _a.$from, $to = _a.$to;
var mark = state.schema.mark('link', { href: utils_1.normalizeUrl(href) });
var tr = empty
? state.tr.insert($from.pos, state.schema.text(text || href, [mark]))
: state.tr.addMark($from.pos, $to.pos, mark);
view.dispatch(tr);
}
};
HyperlinkState.prototype.removeLink = function (view) {
if (this.activeLinkStartPos) {
var state = this.state;
var from = this.activeLinkStartPos;
var to = from + this.text.length;
view.dispatch(state.tr.removeMark(from, to, this.activeLinkMark));
view.focus();
}
};
HyperlinkState.prototype.updateLink = function (options, view) {
if (this.activeLinkStartPos) {
var state = this.state;
var from = this.activeLinkStartPos;
var to = this.activeLinkStartPos + this.text.length;
view.dispatch(state.tr
.removeMark(from, to, this.activeLinkMark)
.addMark(from, to, state.schema.mark('link', { href: utils_1.normalizeUrl(options.href) })));
}
};
HyperlinkState.prototype.updateLinkText = function (text, view) {
if (this.activeLinkStartPos) {
var state = this.state;
var from = this.activeLinkStartPos;
var to = from + (this.text ? this.text.length : 0);
var newTo = from + (text ? text.length : 0);
view.dispatch(state.tr.insertText(text, from, to)
.addMark(from, newTo, this.activeLinkMark));
view.focus();
}
};
HyperlinkState.prototype.update = function (state, docView, dirty) {
if (dirty === void 0) { dirty = false; }
this.state = state;
var nodeInfo = this.getActiveLinkNodeInfo();
var canAddLink = this.isActiveNodeLinkable();
if (canAddLink !== this.linkable) {
this.linkable = canAddLink;
dirty = true;
}
if ((nodeInfo && nodeInfo.node) !== this.activeLinkNode) {
this.activeLinkNode = nodeInfo && nodeInfo.node;
this.activeLinkStartPos = nodeInfo && nodeInfo.startPos;
this.activeLinkMark = nodeInfo && this.getActiveLinkMark(nodeInfo.node);
this.text = nodeInfo && nodeInfo.node.textContent;
this.href = this.activeLinkMark && this.activeLinkMark.attrs.href;
this.active = !!nodeInfo;
dirty = true;
}
this.element = this.getDomElement(docView);
this.activeElement = this.getActiveDomElement(state.selection, docView);
if (dirty) {
this.triggerOnChange();
}
};
HyperlinkState.prototype.escapeFromMark = function (editorView) {
var nodeInfo = this.getActiveLinkNodeInfo();
if (nodeInfo && this.isShouldEscapeFromMark(nodeInfo)) {
var transaction = this.state.tr.removeMark(nodeInfo.startPos, this.state.selection.$from.pos, this.state.schema.marks.link);
editorView.dispatch(transaction);
}
};
HyperlinkState.prototype.showLinkPanel = function (editorView) {
var _this = this;
if (!(this.showToolbarPanel || editorView.hasFocus())) {
editorView.focus();
}
var selection = editorView.state.selection;
if (selection.empty && !this.active) {
this.showToolbarPanel = !this.showToolbarPanel;
this.changeHandlers.forEach(function (cb) { return cb(_this); });
}
else {
this.addLink({ href: '' }, editorView);
this.update(editorView.state, editorView.docView);
}
};
HyperlinkState.prototype.hideLinkPanel = function () {
var _this = this;
this.showToolbarPanel = false;
this.changeHandlers.forEach(function (cb) { return cb(_this); });
};
HyperlinkState.prototype.getCoordinates = function (editorView, offsetParent) {
if (editorView.hasFocus()) {
editorView.focus();
}
var pos = this.state.selection.$from.pos;
var _a = offsetParent.getBoundingClientRect(), left = _a.left, top = _a.top, height = _a.height;
var node = editorView.docView.domFromPos(pos).node;
var cursorNode = (node.nodeType === 3) ?
node.parentNode : node;
var cursorHeight = parseFloat(window.getComputedStyle(cursorNode, undefined).lineHeight || '');
/**
* We need to translate the co-ordinates because `coordsAtPos` returns co-ordinates
* relative to `window`. And, also need to adjust the cursor container height.
* (0, 0)
* +--------------------- [window] ---------------------+
* | (left, top) +-------- [Offset Parent] --------+ |
* | {coordsAtPos} | [Cursor] <- cursorHeight | |
* | | [FloatingToolbar] | |
*/
var translateCoordinates = function (coords, dx, dy) {
return {
left: coords.left - dx,
right: coords.right - dx,
top: (coords.top - dy) + (offsetParent === document.body ? 0 : offsetParent.scrollTop),
bottom: height - (coords.top - dy) - (offsetParent === document.body ? 0 : offsetParent.scrollTop),
};
};
return translateCoordinates(editorView.coordsAtPos(pos), left, top - cursorHeight);
};
HyperlinkState.prototype.triggerOnChange = function () {
var _this = this;
this.changeHandlers.forEach(function (cb) { return cb(_this); });
};
HyperlinkState.prototype.isShouldEscapeFromMark = function (nodeInfo) {
var parentOffset = this.state.selection.$from.parentOffset;
return nodeInfo && parentOffset === 1 && nodeInfo.node.nodeSize > parentOffset;
};
HyperlinkState.prototype.getActiveLinkNodeInfo = function () {
var state = this.state;
var link = state.schema.marks.link;
var _a = state.selection, $from = _a.$from, empty = _a.empty;
if (link && $from) {
var _b = $from.parent.childAfter($from.parentOffset), node = _b.node, offset = _b.offset;
var parentNodeStartPos = $from.start($from.depth);
// offset is the end position of previous node
// This is to check whether the cursor is at the beginning of current node
if (empty && offset + 1 === $from.pos) {
return;
}
if (node && node.isText && link.isInSet(node.marks)) {
return {
node: node,
startPos: parentNodeStartPos + offset
};
}
}
};
HyperlinkState.prototype.getActiveLinkMark = function (activeLinkNode) {
var _this = this;
var linkMarks = activeLinkNode.marks.filter(function (mark) {
return mark.type === _this.state.schema.marks.link;
});
return linkMarks[0];
};
HyperlinkState.prototype.getDomElement = function (docView) {
if (this.activeLinkStartPos) {
var _a = docView.domFromPos(this.activeLinkStartPos), node = _a.node, offset = _a.offset;
if (node.childNodes.length === 0) {
return node.parentNode;
}
return node.childNodes[offset];
}
};
/**
* Returns active dom element for current selection.
* Used by Hyperlink edit popup to position relative to cursor.
*/
HyperlinkState.prototype.getActiveDomElement = function (selection, docView) {
if (selection.$from.pos !== selection.$to.pos) {
return;
}
var node = docView.domFromPos(selection.$from.pos).node;
return node;
};
HyperlinkState.prototype.isActiveNodeLinkable = function () {
var link = this.state.schema.marks.link;
return !!link && commands.toggleMark(link)(this.state);
};
return HyperlinkState;
}());
exports.HyperlinkState = HyperlinkState;
function isReplaceStep(step) {
return !!step && step instanceof prosemirror_1.ReplaceStep;
}
var hasLinkMark = function (schema, node) { return node && schema.marks.link.isInSet(node.marks); };
function updateLinkOnChange(transactions, oldState, newState) {
if (!transactions) {
return;
}
if (transactions.some(function (tr) { return tr.steps.some(isReplaceStep); })) {
var schema = newState.schema;
var _a = oldState.selection.$from, oldNodeAfter = _a.nodeAfter, oldNodeBefore = _a.nodeBefore;
var oldLinkMarkAfter = hasLinkMark(schema, oldNodeAfter);
var oldLinkMarkBefore = hasLinkMark(schema, oldNodeBefore);
var $from = newState.selection.$from;
var newNodeAfter = $from.nodeAfter, newNodeBefore = $from.nodeBefore;
var newLinkMarkAfter = hasLinkMark(schema, newNodeAfter);
var newLinkMarkBefore = hasLinkMark(schema, newNodeBefore);
if (!(oldNodeBefore && oldLinkMarkBefore && newNodeBefore && newLinkMarkBefore)) {
return;
}
var href = void 0;
var end = $from.pos;
var start = end - newNodeBefore.nodeSize;
if (oldNodeAfter && oldLinkMarkAfter &&
oldLinkMarkBefore.attrs.href === utils_1.normalizeUrl("" + oldNodeBefore.text + oldNodeAfter.text)) {
if (newNodeAfter && newLinkMarkAfter) {
// Middle of a link https://goo<|>gle.com/
end += newNodeAfter.nodeSize;
href = "" + newNodeBefore.text + newNodeAfter.text;
}
else {
// Replace end of a link https://goo<|gle.com/|>
href = newNodeBefore.text;
}
}
else if (oldLinkMarkBefore.attrs.href === utils_1.normalizeUrl(oldNodeBefore.text || '')) {
// End of a link https://google.com/<|>
if (newNodeBefore.text !== oldNodeBefore.text) {
href = newNodeBefore.text;
}
}
var match = utils_1.getLinkMatch(href);
if (match || /^[a-z]+:\/\//i.test(href)) {
var tr = newState.tr.removeMark(start, end, schema.marks.link);
if (match) {
var markType = schema.mark('link', { href: match.url });
tr.addMark(start, end, markType);
}
return tr;
}
}
}
exports.plugin = new prosemirror_1.Plugin({
props: {
handleTextInput: function (view, from, to, text) {
var pluginState = plugin_key_1.default.getState(view.state);
pluginState.escapeFromMark(view);
return false;
},
handleClick: function (view) {
var pluginState = plugin_key_1.default.getState(view.state);
if (pluginState.active) {
pluginState.changeHandlers.forEach(function (cb) { return cb(pluginState); });
}
return false;
},
onBlur: function (view) {
var pluginState = plugin_key_1.default.getState(view.state);
pluginState.editorFocused = false;
if (pluginState.active) {
pluginState.changeHandlers.forEach(function (cb) { return cb(pluginState); });
}
return true;
},
onFocus: function (view) {
var pluginState = plugin_key_1.default.getState(view.state);
pluginState.editorFocused = true;
return true;
},
/**
* As we are adding linkifyContent, linkifyText can in fact be removed.
* But leaving it there so that later it can be enhanced to include markdown parsing.
*/
handlePaste: function (view, event, slice) {
var clipboardData = event.clipboardData;
var html = clipboardData && clipboardData.getData('text/html');
if (html) {
var contentSlices = utils_1.linkifyContent(view.state.schema, slice);
if (contentSlices) {
var dispatch = view.dispatch, tr = view.state.tr;
dispatch(tr.replaceSelection(contentSlices));
return true;
}
}
return false;
}
},
state: {
init: function (config, state) {
return new HyperlinkState(state);
},
apply: function (tr, pluginState, oldState, newState) {
return pluginState;
}
},
key: plugin_key_1.default,
view: function (view) {
var pluginState = plugin_key_1.default.getState(view.state);
pluginState.update(view.state, view.docView, true);
return {
update: function (view, prevState) {
pluginState.update(view.state, view.docView);
}
};
},
appendTransaction: function (transactions, oldState, newState) {
return updateLinkOnChange(transactions, oldState, newState);
},
});
var plugins = function (schema, props) {
if (props === void 0) { props = {}; }
return [exports.plugin, input_rule_1.default(schema), keymap_1.default(schema, props)].filter(function (plugin) { return !!plugin; });
};
exports.default = plugins;
//# sourceMappingURL=index.js.map