UNPKG

@atlaskit/editor-core

Version:

A package contains Atlassian editor core functionality

334 lines • 14.5 kB
"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