prosemirror-suggest
Advanced tools
Comparing version 1.0.0-next.0 to 1.0.0-next.1
# prosemirror-suggest | ||
## 1.0.0-next.1 | ||
> 2020-07-05 | ||
### Patch Changes | ||
- Fix missing dist files from previous publish. | ||
- Updated dependencies [undefined] | ||
- @remirror/core-constants@1.0.0-next.1 | ||
- @remirror/core-helpers@1.0.0-next.1 | ||
- @remirror/core-types@1.0.0-next.1 | ||
- @remirror/core-utils@1.0.0-next.1 | ||
## 1.0.0-next.0 | ||
@@ -4,0 +17,0 @@ |
@@ -1,11 +0,1 @@ | ||
// are you seeing an error that a default export doesn't exist but your source file has a default export? | ||
// you should run `yarn` or `yarn preconstruct dev` if preconstruct dev isn't in your postinstall hook | ||
// curious why you need to? | ||
// this file exists so that you can import from the entrypoint normally | ||
// except that it points to your source file and you don't need to run build constantly | ||
// which means we need to re-export all of the modules from your source file | ||
// and since export * doesn't include default exports, we need to read your source file | ||
// to check for a default export and re-export it if it exists | ||
// it's not ideal, but it works pretty well ¯\_(ツ)_/¯ | ||
export * from "../src/index"; | ||
export * from "./declarations/src/index"; |
@@ -1,19 +0,7 @@ | ||
"use strict"; | ||
// this file might look strange and you might be wondering what it's for | ||
// it's lets you import your source files by importing this entrypoint | ||
// as you would import it if it was built with preconstruct build | ||
// this file is slightly different to some others though | ||
// it has a require hook which compiles your code with Babel | ||
// this means that you don't have to set up @babel/register or anything like that | ||
// but you can still require this module and it'll be compiled | ||
'use strict'; | ||
// this bit of code imports the require hook and registers it | ||
let unregister = require("/Users/ifiokjr/Coding/kickjump/remirror/prs/node_modules/.pnpm/@preconstruct/hook@0.1.0/node_modules/@preconstruct/hook/dist/hook.cjs.js").___internalHook("/Users/ifiokjr/Coding/kickjump/remirror/prs"); | ||
// this re-exports the source file | ||
module.exports = require("/Users/ifiokjr/Coding/kickjump/remirror/prs/packages/prosemirror-suggest/src/index.ts"); | ||
// this unregisters the require hook so that any modules required after this one | ||
// aren't compiled with the require hook in case you have some other require hook | ||
// or something that should be used on other modules | ||
unregister(); | ||
if (process.env.NODE_ENV === "production") { | ||
module.exports = require("./prosemirror-suggest.cjs.prod.js"); | ||
} else { | ||
module.exports = require("./prosemirror-suggest.cjs.dev.js"); | ||
} |
@@ -1,15 +0,1240 @@ | ||
// 👋 hey!! | ||
// you might be reading this and seeing .esm in the filename | ||
// and being confused why there is commonjs below this filename | ||
// DON'T WORRY! | ||
// this is intentional | ||
// it's only commonjs with `preconstruct dev` | ||
// when you run `preconstruct build`, it will be ESM | ||
// why is it commonjs? | ||
// we need to re-export every export from the source file | ||
// but we can't do that with ESM without knowing what the exports are (because default exports aren't included in export/import *) | ||
// and they could change after running `preconstruct dev` so we can't look at the file without forcing people to | ||
// run preconstruct dev again which wouldn't be ideal | ||
// this solution could change but for now, it's working | ||
import { Plugin, PluginKey } from 'prosemirror-state'; | ||
import { isSelectionEmpty, hasTransactionChanged, getPluginState } from '@remirror/core-utils'; | ||
import _defineProperty from '@babel/runtime/helpers/esm/defineProperty'; | ||
import _classPrivateFieldSet from '@babel/runtime/helpers/esm/classPrivateFieldSet'; | ||
import _classPrivateFieldGet from '@babel/runtime/helpers/esm/classPrivateFieldGet'; | ||
import mergeDescriptors from 'merge-descriptors'; | ||
import { DecorationSet, Decoration } from 'prosemirror-view'; | ||
import { noop, bool, isString, isRegExp, object, entries, findMatches, isUndefined, isFunction } from '@remirror/core-helpers'; | ||
import _slicedToArray from '@babel/runtime/helpers/esm/slicedToArray'; | ||
import { keydownHandler } from 'prosemirror-keymap'; | ||
import { NULL_CHARACTER } from '@remirror/core-constants'; | ||
import escapeStringRegex from 'escape-string-regexp'; | ||
module.exports = require("/Users/ifiokjr/Coding/kickjump/remirror/prs/packages/prosemirror-suggest/src/index.ts") | ||
var DEFAULT_SUGGEST_ACTIONS = { | ||
command: noop, | ||
create: noop, | ||
remove: noop, | ||
update: noop | ||
}; | ||
var defaultHandler = () => false; | ||
var DEFAULT_SUGGESTER = { | ||
appendText: '', | ||
createCommand: () => noop, | ||
ignoredClassName: undefined, | ||
ignoredTag: 'span', | ||
invalidPrefixCharacters: undefined, | ||
keyBindings: {}, | ||
matchOffset: 0, | ||
noDecorations: false, | ||
onChange: defaultHandler, | ||
onCharacterEntry: defaultHandler, | ||
onExit: defaultHandler, | ||
startOfLine: false, | ||
suggestClassName: 'suggest', | ||
suggestTag: 'span', | ||
supportedCharacters: /\w+/, | ||
validPrefixCharacters: /^[\s\0]?$/ | ||
}; | ||
/** | ||
* The potential reasons for an exit of a mention. | ||
*/ | ||
var ExitReason; | ||
/** | ||
* The potential reason for changes | ||
*/ | ||
(function (ExitReason) { | ||
ExitReason["End"] = "exit-end"; | ||
ExitReason["Removed"] = "delete"; | ||
ExitReason["Split"] = "exit-split"; | ||
ExitReason["InvalidSplit"] = "invalid-exit-split"; | ||
ExitReason["MoveEnd"] = "move-end"; | ||
ExitReason["MoveStart"] = "move-start"; | ||
ExitReason["JumpForward"] = "jump-forward-exit"; | ||
ExitReason["JumpBackward"] = "jump-backward-exit"; | ||
ExitReason["SelectionOutside"] = "selection-outside"; | ||
})(ExitReason || (ExitReason = {})); | ||
var ChangeReason; | ||
(function (ChangeReason) { | ||
ChangeReason["Start"] = "start"; | ||
ChangeReason["Text"] = "change-character"; | ||
ChangeReason["SelectionInside"] = "selection-inside"; | ||
ChangeReason["Move"] = "move"; | ||
ChangeReason["JumpBackward"] = "jump-backward-change"; | ||
ChangeReason["JumpForward"] = "jump-forward-change"; | ||
})(ChangeReason || (ChangeReason = {})); | ||
/** | ||
* Is this a change in the current suggestion (added or deleted characters)? | ||
*/ | ||
var isChange = compare => bool(compare.prev && compare.next && compare.prev.queryText.full !== compare.next.queryText.full); | ||
/** | ||
* Has the cursor moved within the current suggestion (added or deleted | ||
* characters)? | ||
*/ | ||
var isMove = compare => bool(compare.prev && compare.next && compare.prev.range.to !== compare.next.range.to); | ||
/** | ||
* Are we entering a new suggestion? | ||
*/ | ||
var isEntry = compare => bool(!compare.prev && compare.next); | ||
/** | ||
* Are we exiting a suggestion? | ||
*/ | ||
var isExit = compare => bool(compare.prev && !compare.next); | ||
/** | ||
* Is this a jump from one suggestion to another? | ||
*/ | ||
var isJump = compare => bool(compare.prev && compare.next && compare.prev.range.from !== compare.next.range.from); | ||
/** | ||
* Check that the passed in value is an ExitReason | ||
*/ | ||
var isExitReason = value => isString(value) && Object.values(ExitReason).includes(value); | ||
var isChangeReason = value => isString(value) && Object.values(ChangeReason).includes(value); | ||
/** | ||
* Checks that the reason passed is a split reason. This typically means that we | ||
* should default to a partial update / creation of the mention. | ||
*/ | ||
var isSplitReason = value => value === ExitReason.Split; | ||
/** | ||
* Checks that the reason was caused by a split at a point where there is no | ||
* query. | ||
*/ | ||
var isInvalidSplitReason = value => value === ExitReason.InvalidSplit; | ||
/** | ||
* Checks that the reason was caused by a deletion. | ||
*/ | ||
var isRemovedReason = value => value === ExitReason.Removed; | ||
/** | ||
* Checks to see if this is a jump reason. | ||
*/ | ||
var isJumpReason = map => map.exit ? [ExitReason.JumpBackward, ExitReason.JumpForward].includes(map.exit.reason) : map.change ? [ChangeReason.JumpBackward, ChangeReason.JumpForward].includes(map.change.reason) : false; | ||
/** | ||
* True when the match is currently active (i.e. it's query has a value) | ||
*/ | ||
var isValidMatch = match => bool(match && match.queryText.full.length >= match.suggester.matchOffset); | ||
/** | ||
* True when the current selection is outside the match. | ||
*/ | ||
var selectionOutsideMatch = (_ref) => { | ||
var match = _ref.match, | ||
selection = _ref.selection; | ||
return match && (selection.from < match.range.from || selection.from > match.range.end); | ||
}; | ||
var escapeChar = char => escapeStringRegex(char); | ||
/** | ||
* Convert a RegExp into a string | ||
* | ||
* @param regexOrString | ||
*/ | ||
var regexToString = regexOrString => isRegExp(regexOrString) ? regexOrString.source : regexOrString; | ||
/** | ||
* Find regex prefix when depending on whether the mention only supports the | ||
* start of a line or not | ||
* | ||
* @param onlyStartOfLine | ||
*/ | ||
var getRegexPrefix = onlyStartOfLine => onlyStartOfLine ? '^' : ''; | ||
/** | ||
* Get the supported characters regex string. | ||
*/ | ||
var getRegexSupportedCharacters = (supportedCharacters, matchOffset) => "(?:".concat(regexToString(supportedCharacters), "){").concat(matchOffset, ",}"); | ||
/** | ||
* Create a regex expression to evaluate matches directly from the suggester properties. | ||
*/ | ||
var createRegexFromSuggestion = function createRegexFromSuggestion(_ref) { | ||
var char = _ref.char, | ||
matchOffset = _ref.matchOffset, | ||
startOfLine = _ref.startOfLine, | ||
supportedCharacters = _ref.supportedCharacters; | ||
var flags = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'gm'; | ||
return new RegExp("".concat(getRegexPrefix(startOfLine)).concat(escapeChar(char)).concat(getRegexSupportedCharacters(supportedCharacters, matchOffset)), flags); | ||
}; | ||
function _createForOfIteratorHelper(o, allowArrayLike) { var it; if (typeof Symbol === "undefined" || o[Symbol.iterator] == null) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = o[Symbol.iterator](); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it.return != null) it.return(); } finally { if (didErr) throw err; } } }; } | ||
function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } | ||
function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } | ||
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } | ||
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } | ||
/** | ||
* Small utility method for creating a match with the reason property available. | ||
*/ | ||
function createMatchWithReason(parameter) { | ||
var match = parameter.match, | ||
reason = parameter.reason; | ||
return _objectSpread(_objectSpread({}, match), {}, { | ||
reason | ||
}); | ||
} | ||
/** | ||
* Checks to see if the text before the matching character is a valid prefix. | ||
* | ||
* @param prefix - the prefix to test | ||
* @param params - an object with the regex testing values | ||
*/ | ||
var isPrefixValid = (prefix, _ref) => { | ||
var invalidPrefixCharacters = _ref.invalidPrefixCharacters, | ||
validPrefixCharacters = _ref.validPrefixCharacters; | ||
if (!isUndefined(invalidPrefixCharacters)) { | ||
var regex = new RegExp(regexToString(invalidPrefixCharacters)); | ||
return !regex.test(prefix); | ||
} | ||
{ | ||
var _regex = new RegExp(regexToString(validPrefixCharacters)); | ||
return _regex.test(prefix); | ||
} | ||
}; | ||
/** | ||
* Find the position of a mention for a given selection and character | ||
* | ||
* @param params | ||
*/ | ||
var findPosition = (_ref2) => { | ||
var text = _ref2.text, | ||
regexp = _ref2.regexp, | ||
$pos = _ref2.$pos, | ||
char = _ref2.char, | ||
suggester = _ref2.suggester; | ||
var position; | ||
var cursor = $pos.pos; // The current cursor position | ||
var start = $pos.start(); // The starting position for matches | ||
findMatches(text, regexp).forEach(match => { | ||
// Check the character before the current match to ensure it is not one of | ||
// the supported characters | ||
var matchPrefix = match.input.slice(Math.max(0, match.index - 1), match.index); | ||
if (isPrefixValid(matchPrefix, suggester)) { | ||
var from = match.index + start; // The absolute position of the match wrapper node | ||
var end = from + match[0].length; // The position where the match ends | ||
var to = Math.min(end, cursor); // The cursor position (or end position whichever is greater) | ||
var matchLength = to - from; // The length of the current match | ||
// If the $position is located within the matched substring, return that | ||
// range | ||
if (from < cursor && end >= cursor) { | ||
position = { | ||
range: { | ||
from, | ||
end, | ||
to | ||
}, | ||
queryText: { | ||
partial: match[0].slice(char.length, matchLength), | ||
full: match[0].slice(char.length) | ||
}, | ||
matchText: { | ||
partial: match[0].slice(0, matchLength), | ||
full: match[0] | ||
}, | ||
suggester | ||
}; | ||
} | ||
} | ||
}); | ||
return position; | ||
}; | ||
/** | ||
* Checks if any matches exist at the current selection for so that the | ||
* suggesters be activated or deactivated. | ||
*/ | ||
function findMatch(_ref3) { | ||
var $pos = _ref3.$pos, | ||
suggester = _ref3.suggester; | ||
var char = suggester.char, | ||
name = suggester.name, | ||
startOfLine = suggester.startOfLine, | ||
supportedCharacters = suggester.supportedCharacters, | ||
matchOffset = suggester.matchOffset; // Create the regular expression to match the text against | ||
var regexp = createRegexFromSuggestion({ | ||
char, | ||
matchOffset, | ||
startOfLine, | ||
supportedCharacters | ||
}); // All the text in the current node | ||
var text = $pos.doc.textBetween($pos.before(), $pos.end(), NULL_CHARACTER, NULL_CHARACTER); // Find the position and return it | ||
return findPosition({ | ||
suggester, | ||
text, | ||
regexp, | ||
$pos, | ||
char, | ||
name | ||
}); | ||
} | ||
/** | ||
* Checks the provided match and generates a new match. This is useful for | ||
* determining the kind of change that has happened. | ||
* | ||
* If the match still exists and it is different then it's likely a split has | ||
* occurred. | ||
*/ | ||
var recheckMatch = (_ref4) => { | ||
var state = _ref4.state, | ||
match = _ref4.match; | ||
try { | ||
// Wrapped in try catch because it's possible for everything to be deleted | ||
// and the doc.resolve will fail. | ||
return findMatch({ | ||
$pos: state.doc.resolve(match.range.to), | ||
suggester: match.suggester | ||
}); | ||
} catch (_unused) { | ||
return; | ||
} | ||
}; | ||
/** | ||
* Check whether the insert action occurred at the end, in the middle or caused | ||
* the suggestion to be invalid. | ||
* | ||
* Prev refers to the original previous and next refers to the updated version | ||
* after the split | ||
*/ | ||
var createInsertReason = (_ref5) => { | ||
var prev = _ref5.prev, | ||
next = _ref5.next, | ||
state = _ref5.state; | ||
// Has the text been removed? TODO how to tests for deletions mid document? | ||
if (!next && prev.range.from >= state.doc.nodeSize) { | ||
return { | ||
exit: createMatchWithReason({ | ||
match: prev, | ||
reason: ExitReason.Removed | ||
}) | ||
}; | ||
} // Are we within an invalid split? | ||
if (!next || !prev.queryText.partial) { | ||
return { | ||
exit: createMatchWithReason({ | ||
match: prev, | ||
reason: ExitReason.InvalidSplit | ||
}) | ||
}; | ||
} // Are we at the end position? | ||
if (prev.range.end === next.range.to) { | ||
return { | ||
exit: createMatchWithReason({ | ||
match: next, | ||
reason: ExitReason.End | ||
}) | ||
}; | ||
} // Are we in the middle of the mention | ||
if (prev.queryText.partial) { | ||
return { | ||
exit: createMatchWithReason({ | ||
match: next, | ||
reason: ExitReason.Split | ||
}) | ||
}; | ||
} | ||
return {}; | ||
}; | ||
/** | ||
* Find the reason for the Jump | ||
*/ | ||
var findJumpReason = (_ref6) => { | ||
var prev = _ref6.prev, | ||
next = _ref6.next, | ||
state = _ref6.state; | ||
var value = object(); | ||
var updatedPrevious = recheckMatch({ | ||
state, | ||
match: prev | ||
}); | ||
var _ref7 = updatedPrevious && updatedPrevious.queryText.full !== prev.queryText.full // has query changed | ||
? createInsertReason({ | ||
prev, | ||
next: updatedPrevious, | ||
state | ||
}) : value, | ||
exit = _ref7.exit; | ||
var isJumpForward = prev.range.from < next.range.from; | ||
if (isJumpForward) { | ||
return { | ||
exit: exit !== null && exit !== void 0 ? exit : createMatchWithReason({ | ||
match: prev, | ||
reason: ExitReason.JumpForward | ||
}), | ||
change: createMatchWithReason({ | ||
match: next, | ||
reason: ChangeReason.JumpForward | ||
}) | ||
}; | ||
} | ||
return { | ||
exit: exit !== null && exit !== void 0 ? exit : createMatchWithReason({ | ||
match: prev, | ||
reason: ExitReason.JumpBackward | ||
}), | ||
change: createMatchWithReason({ | ||
match: next, | ||
reason: ChangeReason.JumpBackward | ||
}) | ||
}; | ||
}; | ||
/** | ||
* Find the reason for the exit. | ||
* | ||
* This provides some context and helps sets up a helper command with sane | ||
* defaults. | ||
*/ | ||
var findExitReason = (_ref8) => { | ||
var match = _ref8.match, | ||
state = _ref8.state, | ||
$pos = _ref8.$pos; | ||
var selection = state.selection; | ||
var updatedPrevious = recheckMatch({ | ||
match, | ||
state | ||
}); // Exit created a split | ||
if (!updatedPrevious || updatedPrevious.queryText.full !== match.queryText.full) { | ||
return createInsertReason({ | ||
prev: match, | ||
next: updatedPrevious, | ||
state | ||
}); | ||
} // Exit caused by a selection | ||
if (!isSelectionEmpty(state) && (selection.from <= match.range.from || selection.to >= match.range.end)) { | ||
return { | ||
exit: createMatchWithReason({ | ||
match, | ||
reason: ExitReason.SelectionOutside | ||
}) | ||
}; | ||
} // Exit happened at the end of previous suggestion | ||
if ($pos.pos > match.range.end) { | ||
return { | ||
exit: createMatchWithReason({ | ||
match, | ||
reason: ExitReason.MoveEnd | ||
}) | ||
}; | ||
} // Exit happened at the start of previous suggestion | ||
if ($pos.pos <= match.range.from) { | ||
return { | ||
exit: createMatchWithReason({ | ||
match, | ||
reason: ExitReason.MoveStart | ||
}) | ||
}; | ||
} | ||
return {}; | ||
}; | ||
/** | ||
* Transforms the keybindings into an object that can be consumed by the | ||
* prosemirror keydownHandler method. | ||
*/ | ||
function transformKeyBindings(parameter) { | ||
var bindings = parameter.bindings, | ||
suggestionParameter = parameter.suggestionParameter; | ||
var transformed = object(); | ||
var _iterator = _createForOfIteratorHelper(entries(bindings)), | ||
_step; | ||
try { | ||
var _loop = function _loop() { | ||
var _step$value = _slicedToArray(_step.value, 2), | ||
key = _step$value[0], | ||
binding = _step$value[1]; | ||
transformed[key] = () => bool(binding(suggestionParameter)); | ||
}; | ||
for (_iterator.s(); !(_step = _iterator.n()).done;) { | ||
_loop(); | ||
} | ||
} catch (err) { | ||
_iterator.e(err); | ||
} finally { | ||
_iterator.f(); | ||
} | ||
return transformed; | ||
} | ||
/** | ||
* Run the keyBindings when a key is pressed to perform actions. | ||
* | ||
* When return value is `true` no further actions should be taken for this key | ||
* event. When `false` the event will be passed up the chain to the next key | ||
* handler. | ||
* | ||
* This is useful for intercepting events. | ||
*/ | ||
function runKeyBindings(bindings, suggestionParameter) { | ||
return keydownHandler(transformKeyBindings({ | ||
bindings, | ||
suggestionParameter | ||
}))(suggestionParameter.view, suggestionParameter.event); | ||
} | ||
/** | ||
* Creates an array of the actions taken based on the current prev and next | ||
* state field | ||
*/ | ||
var findReason = (_ref9) => { | ||
var prev = _ref9.prev, | ||
next = _ref9.next, | ||
state = _ref9.state, | ||
$pos = _ref9.$pos; | ||
var value = object(); | ||
if (!prev && !next) { | ||
return value; | ||
} | ||
var compare = { | ||
prev, | ||
next | ||
}; // Check for a Jump | ||
if (isJump(compare)) { | ||
return findJumpReason({ | ||
prev: compare.prev, | ||
next: compare.next, | ||
state | ||
}); | ||
} // Entered into a new suggestion | ||
if (isEntry(compare)) { | ||
return { | ||
change: createMatchWithReason({ | ||
match: compare.next, | ||
reason: ChangeReason.Start | ||
}) | ||
}; | ||
} // Exited a suggestion | ||
if (isExit(compare)) { | ||
return findExitReason({ | ||
$pos, | ||
match: compare.prev, | ||
state | ||
}); | ||
} | ||
if (isChange(compare)) { | ||
return { | ||
change: createMatchWithReason({ | ||
match: compare.next, | ||
reason: ChangeReason.Text | ||
}) | ||
}; | ||
} | ||
if (isMove(compare)) { | ||
return { | ||
change: createMatchWithReason({ | ||
match: compare.next, | ||
reason: isSelectionEmpty(state) ? ChangeReason.Move : ChangeReason.SelectionInside | ||
}) | ||
}; | ||
} | ||
return value; | ||
}; | ||
/** | ||
* Find a match for the provided matchers | ||
*/ | ||
function findFromSuggestions(_ref10) { | ||
var suggesters = _ref10.suggesters, | ||
$pos = _ref10.$pos; | ||
// Find the first match and break when done | ||
var _iterator2 = _createForOfIteratorHelper(suggesters), | ||
_step2; | ||
try { | ||
for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) { | ||
var suggester = _step2.value; | ||
try { | ||
var match = findMatch({ | ||
suggester, | ||
$pos | ||
}); | ||
if (match) { | ||
return match; | ||
} | ||
} catch (_unused2) { | ||
console.warn('Error while finding match.'); | ||
} | ||
} | ||
} catch (err) { | ||
_iterator2.e(err); | ||
} finally { | ||
_iterator2.f(); | ||
} | ||
return; | ||
} | ||
function ownKeys$1(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } | ||
function _objectSpread$1(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys$1(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys$1(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } | ||
/** | ||
* The suggestion state which manages the list of suggesters. | ||
*/ | ||
var _ignoreNextExit = /*#__PURE__*/new WeakMap(); | ||
class SuggestState { | ||
/** | ||
* Create an instance of the SuggestState class. | ||
*/ | ||
static create(suggesters) { | ||
return new SuggestState(suggesters); | ||
} | ||
/** | ||
* Returns the current active suggestion state field if one exists | ||
*/ | ||
get match() { | ||
return this.next ? this.next : this.prev && this.handlerMatches.exit ? this.prev : undefined; | ||
} | ||
/** | ||
* Create the state for the `prosemirror-suggest` plugin. | ||
* | ||
* @remarks | ||
* | ||
* Each suggester must provide a name value which is globally unique since it | ||
* acts as the identifier. | ||
* | ||
* It is possible to register multiple suggesters with identical `char` | ||
* properties. The matched suggester is based on the specificity of the | ||
* `regex` and the order in which they are passed in. Earlier suggesters are | ||
* prioritized. | ||
*/ | ||
constructor(suggesters) { | ||
_ignoreNextExit.set(this, { | ||
writable: true, | ||
value: false | ||
}); | ||
_defineProperty(this, "handlerMatches", object()); | ||
_defineProperty(this, "ignored", DecorationSet.empty); | ||
_defineProperty(this, "removed", false); | ||
_defineProperty(this, "setRemovedTrue", () => { | ||
this.removed = true; | ||
}); | ||
_defineProperty(this, "onViewUpdate", () => { | ||
var match = this.match, | ||
_this$handlerMatches = this.handlerMatches, | ||
change = _this$handlerMatches.change, | ||
exit = _this$handlerMatches.exit; | ||
var shouldRunExit = () => { | ||
if (_classPrivateFieldGet(this, _ignoreNextExit)) { | ||
_classPrivateFieldSet(this, _ignoreNextExit, false); | ||
return false; | ||
} | ||
return true; | ||
}; // Cancel update when a suggestion isn't active | ||
if (!change && !exit || !isValidMatch(match)) { | ||
return; | ||
} // When a jump happens run the action that involves the | ||
// position that occurs later in the document. This is so that changes don't | ||
// affect previous positions. | ||
if (change && exit && isJumpReason({ | ||
change, | ||
exit | ||
})) { | ||
var exitParameters = this.createReasonParameter(exit); | ||
var changeParameters = this.createReasonParameter(change); | ||
var movedForwards = exit.range.from < change.range.from; | ||
if (movedForwards) { | ||
change.suggester.onChange(changeParameters); | ||
shouldRunExit() && exit.suggester.onExit(exitParameters); | ||
} else { | ||
shouldRunExit() && exit.suggester.onExit(exitParameters); | ||
change.suggester.onChange(changeParameters); | ||
} | ||
this.removed = false; | ||
return; | ||
} | ||
if (change) { | ||
change.suggester.onChange(this.createReasonParameter(change)); | ||
} | ||
if (exit && shouldRunExit()) { | ||
exit.suggester.onExit(this.createReasonParameter(exit)); | ||
this.removed = false; | ||
if (isInvalidSplitReason(exit.reason)) { | ||
this.handlerMatches = object(); | ||
} | ||
} | ||
}); | ||
_defineProperty(this, "ignoreNextExit", () => { | ||
_classPrivateFieldSet(this, _ignoreNextExit, true); | ||
}); | ||
_defineProperty(this, "addIgnored", (_ref) => { | ||
var from = _ref.from, | ||
char = _ref.char, | ||
name = _ref.name, | ||
_ref$specific = _ref.specific, | ||
specific = _ref$specific === void 0 ? false : _ref$specific; | ||
var to = from + char.length; | ||
var suggester = this.suggesters.find(value => value.name === name); | ||
if (!suggester) { | ||
throw new Error("No suggester exists for the name provided: ".concat(name)); | ||
} | ||
var attributes = suggester.ignoredClassName ? { | ||
class: suggester.ignoredClassName | ||
} : {}; | ||
var decoration = Decoration.inline(from, to, _objectSpread$1({ | ||
nodeName: suggester.ignoredTag | ||
}, attributes), { | ||
char, | ||
name, | ||
specific | ||
}); | ||
this.ignored = this.ignored.add(this.view.state.doc, [decoration]); | ||
}); | ||
_defineProperty(this, "removeIgnored", (_ref2) => { | ||
var from = _ref2.from, | ||
char = _ref2.char, | ||
name = _ref2.name; | ||
var decorations = this.ignored.find(from, from + char.length); | ||
var decoration = decorations[0]; | ||
if (!bool(decoration) || decoration.spec.name !== name) { | ||
return; | ||
} | ||
this.ignored = this.ignored.remove([decoration]); | ||
}); | ||
_defineProperty(this, "clearIgnored", name => { | ||
if (name) { | ||
var decorations = this.ignored.find(); | ||
var decorationsToClear = decorations.filter((_ref3) => { | ||
var spec = _ref3.spec; | ||
return spec.name === name; | ||
}); | ||
this.ignored = this.ignored.remove(decorationsToClear); | ||
} else { | ||
this.ignored = DecorationSet.empty; | ||
} | ||
}); | ||
var names = []; | ||
this.suggesters = suggesters.map(suggester => { | ||
if (names.includes(suggester.name)) { | ||
throw new Error("A suggester already exists with the name '".concat(suggester.name, "'. The name provided must be unique.")); | ||
} | ||
var clone = _objectSpread$1(_objectSpread$1({}, DEFAULT_SUGGESTER), suggester); // Preserve any descriptors (getters and setters) | ||
mergeDescriptors(clone, suggester); | ||
names.push(suggester.name); | ||
return clone; | ||
}); | ||
} | ||
/** | ||
* Initialize the SuggestState with a view which is stored for use later. | ||
*/ | ||
init(view) { | ||
this.view = view; | ||
return this; | ||
} | ||
/** | ||
* Sets the removed property to be true. This is passed as a property to the | ||
* `createCommand` option. | ||
*/ | ||
/** | ||
* The actions created by the extension. | ||
*/ | ||
getCommand(match, reason) { | ||
return match.suggester.createCommand({ | ||
match, | ||
reason, | ||
view: this.view, | ||
setMarkRemoved: this.setRemovedTrue, | ||
addIgnored: this.addIgnored, | ||
clearIgnored: this.clearIgnored, | ||
ignoreNextExit: this.ignoreNextExit | ||
}); | ||
} | ||
/** | ||
* Create the props which should be passed into each action handler | ||
*/ | ||
createParameter(match) { | ||
return _objectSpread$1({ | ||
view: this.view, | ||
addIgnored: this.addIgnored, | ||
clearIgnored: this.clearIgnored, | ||
ignoreNextExit: this.ignoreNextExit, | ||
command: this.getCommand(match) | ||
}, match); | ||
} | ||
/** | ||
* Create the prop to be passed into the `onChange` or `onExit` handler. | ||
*/ | ||
createReasonParameter(match) { | ||
return _objectSpread$1(_objectSpread$1({}, this.createParameter(match)), {}, { | ||
command: this.getCommand(match, match.reason) | ||
}, match); | ||
} | ||
/** | ||
* Manages the view updates. | ||
*/ | ||
/** | ||
* Update the current ignored decorations based on the latest changes to the | ||
* prosemirror document. | ||
*/ | ||
mapIgnoredDecorations(tr) { | ||
// Map over and update the ignored decorations. | ||
var ignored = this.ignored.map(tr.mapping, tr.doc); | ||
var decorations = ignored.find(); // For suggesters with multiple characters it is possible for a `paste` or | ||
// any edit action within the decoration to expand the ignored section. We | ||
// check for that here and if the section size has changed it should be | ||
// marked as invalid and removed from the ignored `DecorationSet`. | ||
var invalid = decorations.filter((_ref4) => { | ||
var from = _ref4.from, | ||
to = _ref4.to, | ||
spec = _ref4.spec; | ||
if (to - from !== spec.char.length) { | ||
return true; | ||
} | ||
return false; | ||
}); | ||
this.ignored = ignored.remove(invalid); | ||
} | ||
shouldIgnoreMatch(_ref5) { | ||
var range = _ref5.range, | ||
name = _ref5.suggester.name; | ||
var decorations = this.ignored.find(); | ||
return decorations.some((_ref6) => { | ||
var spec = _ref6.spec, | ||
from = _ref6.from; | ||
if (from !== range.from) { | ||
return false; | ||
} | ||
var shouldIgnore = spec.specific ? spec.name === name : true; | ||
return shouldIgnore; | ||
}); | ||
} | ||
/** | ||
* Reset the state. | ||
*/ | ||
resetState() { | ||
this.handlerMatches = object(); | ||
this.next = undefined; | ||
this.removed = false; | ||
} | ||
/** | ||
* Update the next state value. | ||
*/ | ||
updateReasons(_ref7) { | ||
var $pos = _ref7.$pos, | ||
state = _ref7.state; | ||
var match = findFromSuggestions({ | ||
suggesters: this.suggesters, | ||
$pos | ||
}); | ||
this.next = match && this.shouldIgnoreMatch(match) ? undefined : match; // Store the matches with reasons | ||
this.handlerMatches = findReason({ | ||
next: this.next, | ||
prev: this.prev, | ||
state, | ||
$pos | ||
}); | ||
} | ||
/** | ||
* Used to handle the view property of the plugin spec. | ||
*/ | ||
viewHandler() { | ||
return { | ||
update: this.onViewUpdate | ||
}; | ||
} | ||
toJSON() { | ||
return this.match; | ||
} | ||
/** | ||
* Applies updates to the state to be used within the plugins apply method. | ||
* | ||
* @param - params | ||
*/ | ||
apply(_ref8) { | ||
var tr = _ref8.tr, | ||
newState = _ref8.newState; | ||
var exit = this.handlerMatches.exit; | ||
if (!hasTransactionChanged(tr) && !this.removed) { | ||
return this; | ||
} | ||
this.mapIgnoredDecorations(tr); // If the previous run was an exit reset the suggestion matches | ||
if (exit) { | ||
this.resetState(); | ||
} | ||
this.prev = this.next; // Match against the current selection position | ||
this.updateReasons({ | ||
$pos: tr.selection.$from, | ||
state: newState | ||
}); | ||
return this; | ||
} | ||
/** | ||
* Manages the keyDown event within the plugin props | ||
* | ||
* @param event | ||
*/ | ||
handleKeyDown(event) { | ||
var match = this.match; | ||
if (!isValidMatch(match)) { | ||
return false; | ||
} | ||
var keyBindings = match.suggester.keyBindings; | ||
var parameter = _objectSpread$1({ | ||
event, | ||
setMarkRemoved: this.setRemovedTrue | ||
}, this.createParameter(match)); // TODO recalculating the keybindings on every update this is a performance bottleneck | ||
return runKeyBindings(isFunction(keyBindings) ? keyBindings() : keyBindings, parameter); | ||
} | ||
/** | ||
* Handle any key presses of non supported characters | ||
*/ | ||
handleTextInput(_ref9) { | ||
var text = _ref9.text, | ||
from = _ref9.from, | ||
to = _ref9.to; | ||
var match = this.match; | ||
if (!isValidMatch(match)) { | ||
return false; | ||
} | ||
var onCharacterEntry = match.suggester.onCharacterEntry; | ||
return onCharacterEntry(_objectSpread$1(_objectSpread$1({}, this.createParameter(match)), {}, { | ||
from, | ||
to, | ||
text | ||
})); | ||
} | ||
/** | ||
* Handle the decorations which wrap the mention while it is active and not | ||
* yet complete. | ||
*/ | ||
decorations(state) { | ||
var match = this.match; | ||
if (!isValidMatch(match)) { | ||
return this.ignored; | ||
} | ||
if (match.suggester.noDecorations) { | ||
return this.ignored; | ||
} | ||
var range = match.range, | ||
_match$suggester = match.suggester, | ||
name = _match$suggester.name, | ||
decorationsTag = _match$suggester.suggestTag, | ||
suggestionClassName = _match$suggester.suggestClassName; | ||
var from = range.from, | ||
end = range.end; | ||
return this.shouldIgnoreMatch(match) ? this.ignored : this.ignored.add(state.doc, [Decoration.inline(from, end, { | ||
nodeName: decorationsTag, | ||
class: name ? "".concat(suggestionClassName, " ").concat(suggestionClassName, "-").concat(name) : suggestionClassName | ||
})]); | ||
} | ||
} | ||
var suggestPluginKey = /*#__PURE__*/new PluginKey('suggest'); | ||
/** | ||
* Get the state of the suggest plugin. | ||
* | ||
* @param state - the editor state. | ||
*/ | ||
function getSuggestPluginState(state) { | ||
return getPluginState(suggestPluginKey, state); | ||
} | ||
/** | ||
* This creates a suggestion plugin with all the suggesters provided. | ||
* | ||
* @remarks | ||
* | ||
* In the following example we're creating an emoji suggestion plugin that | ||
* responds to the colon character with a query and presents a list of matching | ||
* emojis based on the query typed so far. | ||
* | ||
* ```ts | ||
* import { Suggestion, suggest } from 'prosemirror-suggest'; | ||
* | ||
* const maxResults = 10; | ||
* let selectedIndex = 0; | ||
* let emojiList: string[] = []; | ||
* let showSuggestions = false; | ||
* | ||
* const suggestEmojis: Suggestion = { | ||
* // By default decorations are used to highlight the currently matched | ||
* // suggestion in the dom. | ||
* // In this example we don't need decorations (in fact they cause problems when the | ||
* // emoji string replaces the query text in the dom). | ||
* noDecorations: true, | ||
* char: ':', // The character to match against | ||
* name: 'emoji-suggestion', // a unique name | ||
* appendText: '', // Text to append to the created match | ||
* | ||
* // Keybindings are similar to prosemirror keymaps with a few extra niceties. | ||
* // The key identifier can also include modifiers (e.g.) `Cmd-Space: () => false` | ||
* // Return true to prevent any further keyboard handlers from running. | ||
* keyBindings: { | ||
* ArrowUp: () => { | ||
* selectedIndex = rotateSelectionBackwards(selectedIndex, emojiList.length); | ||
* }, | ||
* ArrowDown: () => { | ||
* selectedIndex = rotateSelectionForwards(selectedIndex, emojiList.length); | ||
* }, | ||
* Enter: ({ command }) => { | ||
* if (showSuggestions) { | ||
* command(emojiList[selectedIndex]); | ||
* } | ||
* }, | ||
* Esc: () => { | ||
* showSuggestions = false; | ||
* }, | ||
* }, | ||
* | ||
* onChange: params => { | ||
* const query = params.query.full; | ||
* emojiList = sortEmojiMatches({ query, maxResults }); | ||
* selectedIndex = 0; | ||
* showSuggestions = true; | ||
* }, | ||
* | ||
* onExit: () => { | ||
* showSuggestions = false; | ||
* emojiList = []; | ||
* selectedIndex = 0; | ||
* }, | ||
* | ||
* // Create a function that is passed into the change, exit and keybinding handlers. | ||
* // This is useful when these handlers are called in a different part of the app. | ||
* createCommand: ({ match, view }) => { | ||
* return (emoji,skinVariation) => { | ||
* if (!emoji) { | ||
* throw new Error('An emoji is required when calling the emoji suggesters command'); | ||
* } | ||
* | ||
* const tr = view.state.tr; const { from, end: to } = match.range; | ||
* tr.insertText(emoji, from, to); view.dispatch(tr); | ||
* }; | ||
* }, | ||
* }; | ||
* | ||
* // Create the plugin with the above configuration. It also supports multiple plugins being added. | ||
* const suggestionPlugin = suggest(suggestEmojis); | ||
* | ||
* // Include the plugin in the created editor state. | ||
* const state = EditorState.create({schema, | ||
* plugins: [suggestionPlugin], | ||
* }); | ||
* ``` | ||
* | ||
* The priority of the suggesters is the order in which they are passed into | ||
* this function. | ||
* | ||
* - `const plugin = suggest(two, one, three)` - Here `two` will be checked | ||
* first, then `one` and then `three`. | ||
* | ||
* Only one suggestion can match at any given time. The order and specificity of | ||
* the regex parameters help determines which suggestion will be active. | ||
* | ||
* @param suggesters - a list of suggesters in the order they should be | ||
* evaluated. | ||
*/ | ||
function suggest() { | ||
for (var _len = arguments.length, suggesters = new Array(_len), _key = 0; _key < _len; _key++) { | ||
suggesters[_key] = arguments[_key]; | ||
} | ||
var pluginState = SuggestState.create(suggesters); | ||
return new Plugin({ | ||
key: suggestPluginKey, | ||
// Handle the plugin view | ||
view: _view => { | ||
return pluginState.init(_view).viewHandler(); | ||
}, | ||
state: { | ||
// Initialize the state | ||
init: () => { | ||
return pluginState; | ||
}, | ||
// Apply changes to the state | ||
apply: (tr, _, oldState, newState) => { | ||
return pluginState.apply({ | ||
tr, | ||
oldState, | ||
newState | ||
}); | ||
} | ||
}, | ||
props: { | ||
// Call the keydown hook if suggestion is active. | ||
handleKeyDown: (_, event) => { | ||
return pluginState.handleKeyDown(event); | ||
}, | ||
// Defer to the pluginState handler | ||
handleTextInput: (_, from, to, text) => { | ||
return pluginState.handleTextInput({ | ||
text, | ||
from, | ||
to | ||
}); | ||
}, | ||
// Sets up a decoration (styling options) on the currently active | ||
// decoration | ||
decorations: state => { | ||
return pluginState.decorations(state); | ||
} | ||
} | ||
}); | ||
} | ||
export { ChangeReason, DEFAULT_SUGGESTER, DEFAULT_SUGGEST_ACTIONS, ExitReason, createRegexFromSuggestion, escapeChar, getRegexPrefix, getSuggestPluginState, isChange, isChangeReason, isEntry, isExit, isExitReason, isInvalidSplitReason, isJump, isJumpReason, isMove, isRemovedReason, isSplitReason, isValidMatch, regexToString, selectionOutsideMatch, suggest }; |
{ | ||
"name": "prosemirror-suggest", | ||
"version": "1.0.0-next.0", | ||
"version": "1.0.0-next.1", | ||
"description": "Primitives for building your prosemirror suggestion and autocomplete functionality", | ||
@@ -20,6 +20,6 @@ "homepage": "https://github.com/remirror/remirror/tree/next/packages/prosemirror-suggest", | ||
"@babel/runtime": "^7.10.4", | ||
"@remirror/core-constants": "^1.0.0-next.0", | ||
"@remirror/core-helpers": "^1.0.0-next.0", | ||
"@remirror/core-types": "^1.0.0-next.0", | ||
"@remirror/core-utils": "^1.0.0-next.0", | ||
"@remirror/core-constants": "^1.0.0-next.1", | ||
"@remirror/core-helpers": "^1.0.0-next.1", | ||
"@remirror/core-types": "^1.0.0-next.1", | ||
"@remirror/core-utils": "^1.0.0-next.1", | ||
"@types/merge-descriptors": "1.0.1", | ||
@@ -26,0 +26,0 @@ "@types/prosemirror-keymap": "^1.0.2", |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
181648
17
3912
2