diff --git a/package.json b/package.json index 43c9bbc..b3f1e7b 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "@ckeditor/ckeditor5-core": "^16.0.0", "@ckeditor/ckeditor5-ui": "^16.0.0", "@ckeditor/ckeditor5-typing": "^16.0.0", - "@ckeditor/ckeditor5-utils": "^16.0.0" + "@ckeditor/ckeditor5-utils": "^16.0.0", + "lodash-es": "^4.17.10" }, "devDependencies": { "@ckeditor/ckeditor5-basic-styles": "^16.0.0", @@ -31,6 +32,7 @@ "eslint-config-ckeditor5": "^2.0.0", "husky": "^1.3.1", "lint-staged": "^7.0.0", + "lodash": "^4.17.11", "stylelint": "^11.1.1", "stylelint-config-ckeditor5": "^1.0.0" }, diff --git a/src/mentionediting.js b/src/mentionediting.js index b45cb78..366b661 100644 --- a/src/mentionediting.js +++ b/src/mentionediting.js @@ -81,7 +81,7 @@ export function _addMentionAttributes( baseMentionData, data ) { * @protected * @param {module:engine/view/element~Element} viewElementOrMention * @param {String|Object} [data] Mention data to be extended. - * @return {module:mention/mention~MentionAttribute} + * @returns {module:mention/mention~MentionAttribute} */ export function _toMentionAttribute( viewElementOrMention, data ) { const dataMention = viewElementOrMention.getAttribute( 'data-mention' ); diff --git a/src/mentionui.js b/src/mentionui.js index 796ad66..a17be74 100644 --- a/src/mentionui.js +++ b/src/mentionui.js @@ -7,6 +7,8 @@ * @module mention/mentionui */ +/* global console */ + import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview'; import Collection from '@ckeditor/ckeditor5-utils/src/collection'; @@ -14,8 +16,9 @@ import clickOutsideHandler from '@ckeditor/ckeditor5-ui/src/bindings/clickoutsid import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard'; import env from '@ckeditor/ckeditor5-utils/src/env'; import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect'; -import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; +import CKEditorError, { attachLinkToDocumentation } from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; import ContextualBalloon from '@ckeditor/ckeditor5-ui/src/panel/balloon/contextualballoon'; +import { debounce } from 'lodash-es'; import TextWatcher from '@ckeditor/ckeditor5-typing/src/textwatcher'; @@ -25,6 +28,16 @@ import MentionListItemView from './ui/mentionlistitemview'; const VERTICAL_SPACING = 3; +// The key codes that mention UI handles when it is open. +const handledKeyCodes = [ + keyCodes.arrowup, + keyCodes.arrowdown, + keyCodes.enter, + keyCodes.tab, + keyCodes.space, + keyCodes.esc +]; + /** * The mention UI feature. * @@ -38,6 +51,13 @@ export default class MentionUI extends Plugin { return 'MentionUI'; } + /** + * @inheritDoc + */ + static get requires() { + return [ ContextualBalloon ]; + } + /** * @inheritDoc */ @@ -60,6 +80,16 @@ export default class MentionUI extends Plugin { */ this._mentionsConfigurations = new Map(); + /** + * Debounced feed requester. It uses `lodash#debounce` method to delay function call. + * + * @private + * @param {String} marker + * @param {String} feedText + * @method + */ + this._requestFeedDebounced = debounce( this._requestFeed, 100 ); + editor.config.define( 'mention', { feeds: [] } ); } @@ -116,7 +146,7 @@ export default class MentionUI extends Plugin { const marker = mentionDescription.marker; - if ( !marker || marker.length != 1 ) { + if ( !isValidMentionMarker( marker ) ) { /** * The marker must be a single character. * @@ -143,6 +173,9 @@ export default class MentionUI extends Plugin { this._mentionsConfigurations.set( marker, definition ); } + + this.on( 'requestFeed:response', ( evt, data ) => this._handleFeedResponse( data ) ); + this.on( 'requestFeed:error', () => this._hideUIAndRemoveMarker() ); } /** @@ -155,13 +188,6 @@ export default class MentionUI extends Plugin { this._mentionsView.destroy(); } - /** - * @inheritDoc - */ - static get requires() { - return [ ContextualBalloon ]; - } - /** * Returns true when {@link #_mentionsView} is in the {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon} and it is * currently visible. @@ -252,17 +278,80 @@ export default class MentionUI extends Plugin { } /** - * Returns a promise that resolves with autocomplete items for a given text. + * Requests a feed from a configured callbacks. * + * @private + * @fires module:mention/mentionui~MentionUI#event:requestFeed:response + * @fires module:mention/mentionui~MentionUI#event:requestFeed:discarded + * @fires module:mention/mentionui~MentionUI#event:requestFeed:error * @param {String} marker * @param {String} feedText - * @return {Promise} - * @private */ - _getFeed( marker, feedText ) { + _requestFeed( marker, feedText ) { + // Store the last requested feed - it is used to discard any out-of order requests. + this._lastRequested = feedText; + const { feedCallback } = this._mentionsConfigurations.get( marker ); + const feedResponse = feedCallback( feedText ); + + const isAsynchronous = feedResponse instanceof Promise; + + // For synchronous feeds (e.g. callbacks, arrays) fire the response event immediately. + if ( !isAsynchronous ) { + /** + * Fired whenever requested feed has a response. + * + * @event requestFeed:response + * @param {Object} data Event data. + * @param {Array.} data.feed Autocomplete items. + * @param {String} data.marker The character which triggers autocompletion for mention. + * @param {String} data.feedText The text for which feed items were requested. + */ + this.fire( 'requestFeed:response', { feed: feedResponse, marker, feedText } ); + + return; + } + + // Handle the asynchronous responses. + feedResponse + .then( response => { + // Check the feed text of this response with the last requested one so either: + if ( this._lastRequested == feedText ) { + // It is the same and fire the response event. + this.fire( 'requestFeed:response', { feed: response, marker, feedText } ); + } else { + // It is different - most probably out-of-order one, so fire the discarded event. + /** + * Fired whenever the requested feed was discarded. This happens when the response was delayed and + * other feed was already requested. + * + * @event requestFeed:discarded + * @param {Object} data Event data. + * @param {Array.} data.feed Autocomplete items. + * @param {String} data.marker The character which triggers autocompletion for mention. + * @param {String} data.feedText The text for which feed items were requested. + */ + this.fire( 'requestFeed:discarded', { feed: response, marker, feedText } ); + } + } ) + .catch( error => { + /** + * Fired whenever the requested {@link module:mention/mention~MentionFeed#feed} promise fails with error. + * + * @event requestFeed:error + * @param {Object} data Event data. + * @param {Error} data.error The error that was caught. + */ + this.fire( 'requestFeed:error', { error } ); - return Promise.resolve().then( () => feedCallback( feedText ) ); + /** + * The callback used for obtaining mention autocomplete feed thrown and error and the mention UI was hidden or + * not displayed at all. + * + * @error mention-feed-callback-error + */ + console.warn( attachLinkToDocumentation( 'mention-feed-callback-error: Could not obtain mention autocomplete feed.' ) ); + } ); } /** @@ -282,20 +371,13 @@ export default class MentionUI extends Plugin { const selection = editor.model.document.selection; const focus = selection.focus; - // The text watcher listens only to changed range in selection - so the selection attributes are not yet available - // and you cannot use selection.hasAttribute( 'mention' ) just yet. - // See https://github.com/ckeditor/ckeditor5-engine/issues/1723. - const hasMention = focus.textNode && focus.textNode.hasAttribute( 'mention' ); - - const nodeBefore = focus.nodeBefore; - - if ( hasMention || nodeBefore && nodeBefore.is( 'text' ) && nodeBefore.hasAttribute( 'mention' ) ) { + if ( hasExistingMention( focus ) ) { this._hideUIAndRemoveMarker(); return; } - const feedText = getFeedText( marker, data.text ); + const feedText = requestFeedText( marker, data.text ); const matchedTextLength = marker.length + feedText.length; // Create a marker range. @@ -304,34 +386,20 @@ export default class MentionUI extends Plugin { const markerRange = editor.model.createRange( start, end ); - let mentionMarker; + if ( checkIfStillInCompletionMode( editor ) ) { + const mentionMarker = editor.model.markers.get( 'mention' ); - if ( editor.model.markers.has( 'mention' ) ) { - mentionMarker = editor.model.markers.get( 'mention' ); + // Update the marker - user might've moved the selection to other mention trigger. + editor.model.change( writer => { + writer.updateMarker( mentionMarker, { range: markerRange } ); + } ); } else { - mentionMarker = editor.model.change( writer => writer.addMarker( 'mention', { - range: markerRange, - usingOperation: false, - affectsData: false - } ) ); + editor.model.change( writer => { + writer.addMarker( 'mention', { range: markerRange, usingOperation: false, affectsData: false } ); + } ); } - this._getFeed( marker, feedText ) - .then( feed => { - this._items.clear(); - - for ( const feedItem of feed ) { - const item = typeof feedItem != 'object' ? { id: feedItem, text: feedItem } : feedItem; - - this._items.add( { item, marker } ); - } - - if ( this._items.length ) { - this._showUI( mentionMarker ); - } else { - this._hideUIAndRemoveMarker(); - } - } ); + this._requestFeedDebounced( marker, feedText ); } ); watcher.on( 'unmatched', () => { @@ -341,12 +409,45 @@ export default class MentionUI extends Plugin { return watcher; } + /** + * Handles the feed response event data. + * + * @param data + * @private + */ + _handleFeedResponse( data ) { + const { feed, marker } = data; + + // If the marker is not in the document happens when the selection had changed and the 'mention' marker was removed. + if ( !checkIfStillInCompletionMode( this.editor ) ) { + return; + } + + // Reset the view. + this._items.clear(); + + for ( const feedItem of feed ) { + const item = typeof feedItem != 'object' ? { id: feedItem, text: feedItem } : feedItem; + + this._items.add( { item, marker } ); + } + + const mentionMarker = this.editor.model.markers.get( 'mention' ); + + if ( this._items.length ) { + this._showOrUpdateUI( mentionMarker ); + } else { + // Do not show empty mention UI. + this._hideUIAndRemoveMarker(); + } + } + /** * Shows the mentions balloon. If the panel is already visible, it will reposition it. * * @private */ - _showUI( markerMarker ) { + _showOrUpdateUI( markerMarker ) { if ( this._isUIVisible ) { // Update balloon position as the mention list view may change its size. this._balloon.updatePosition( this._getBalloonPanelPositionData( markerMarker, this._mentionsView.position ) ); @@ -360,7 +461,6 @@ export default class MentionUI extends Plugin { } this._mentionsView.position = this._balloon.view.position; - this._mentionsView.selectFirst(); } @@ -375,7 +475,7 @@ export default class MentionUI extends Plugin { this._balloon.remove( this._mentionsView ); } - if ( this.editor.model.markers.has( 'mention' ) ) { + if ( checkIfStillInCompletionMode( this.editor ) ) { this.editor.model.change( writer => writer.removeMarker( 'mention' ) ); } @@ -432,7 +532,7 @@ export default class MentionUI extends Plugin { */ _getBalloonPanelPositionData( mentionMarker, preferredPosition ) { const editor = this.editor; - const editing = this.editor.editing; + const editing = editor.editing; const domConverter = editing.view.domConverter; const mapper = editing.mapper; @@ -566,7 +666,7 @@ function createTestCallback( marker, minimumCharacters ) { // // @param {String} marker // @returns {Function} -function getFeedText( marker, text ) { +function requestFeedText( marker, text ) { const regExp = createRegExp( marker, 0 ); const match = text.match( regExp ); @@ -589,7 +689,7 @@ function createFeedCallback( feedItems ) { // Do not return more than 10 items. .slice( 0, 10 ); - return Promise.resolve( filteredItems ); + return filteredItems; }; } @@ -598,14 +698,35 @@ function createFeedCallback( feedItems ) { // @param {Number} // @returns {Boolean} function isHandledKey( keyCode ) { - const handledKeyCodes = [ - keyCodes.arrowup, - keyCodes.arrowdown, - keyCodes.enter, - keyCodes.tab, - keyCodes.space, - keyCodes.esc - ]; - return handledKeyCodes.includes( keyCode ); } + +// Checks if position in inside or right after a text with a mention. +// +// @param {module:engine/model/position~Position} position. +// @returns {Boolean} +function hasExistingMention( position ) { + // The text watcher listens only to changed range in selection - so the selection attributes are not yet available + // and you cannot use selection.hasAttribute( 'mention' ) just yet. + // See https://github.com/ckeditor/ckeditor5-engine/issues/1723. + const hasMention = position.textNode && position.textNode.hasAttribute( 'mention' ); + + const nodeBefore = position.nodeBefore; + + return hasMention || nodeBefore && nodeBefore.is( 'text' ) && nodeBefore.hasAttribute( 'mention' ); +} + +// Checks if string is a valid mention marker. +// +// @param {String} marker +// @returns {Boolean} +function isValidMentionMarker( marker ) { + return marker && marker.length == 1; +} + +// Checks the mention plugins is in completion mode (e.g. when typing is after a valid mention string like @foo). +// +// @returns {Boolean} +function checkIfStillInCompletionMode( editor ) { + return editor.model.markers.has( 'mention' ); +} diff --git a/tests/_utils/asyncserver/data/db.json b/tests/_utils/asyncserver/data/db.json new file mode 100644 index 0000000..86f8bce --- /dev/null +++ b/tests/_utils/asyncserver/data/db.json @@ -0,0 +1 @@ +[{"name":{"title":"mrs","first":"faustine","last":"michel"},"email":"faustine.michel@example.com","login":{"uuid":"6b5156d4-76af-48f0-886e-1f880c3d540c","username":"tinybird969","password":"heaven","salt":"xP7I8cpI","md5":"22b9ec8d6ed0df03d301d9c8af691722","sha1":"18bcfc1a8b22b30061466169c16b7f01db8ccff9","sha256":"ce131a228446ab8d0c5f8ae98e28e6ed84622fc675e5b3210af1d06ba1f2932b"},"picture":{"large":"https://randomuser.me/api/portraits/women/2.jpg","medium":"https://randomuser.me/api/portraits/med/women/2.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/2.jpg"}},{"name":{"title":"mr","first":"antonio","last":"fernandez"},"email":"antonio.fernandez@example.com","login":{"uuid":"19c1df69-5424-495a-a095-07829d1998ce","username":"organicleopard805","password":"masters","salt":"xdEFkM1e","md5":"87da3493110a4a6f2b170d1b03c9f0b3","sha1":"9437c28fd3cabfa34890e1ca819ae6dfd9220b6d","sha256":"cc4e4a828c6a11243c0a6cf6a466d4b4fd487c3a8f24c516b211d8aa532aa507"},"picture":{"large":"https://randomuser.me/api/portraits/men/15.jpg","medium":"https://randomuser.me/api/portraits/med/men/15.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/men/15.jpg"}},{"name":{"title":"mr","first":"absalão","last":"porto"},"email":"absalão.porto@example.com","login":{"uuid":"d08172cd-9957-48eb-8ce6-1f4632100ccc","username":"happymeercat996","password":"starr","salt":"Fs9Ked4l","md5":"bb53e36b1d0c8d2bf3320e3e1de7a897","sha1":"b06cb90233a9ea32b4165e13d689b4c983d2aa4a","sha256":"ba632ee7f1888d9976df13f66c3159561741e4128ef5cab08c8bfc21b58f5811"},"picture":{"large":"https://randomuser.me/api/portraits/men/2.jpg","medium":"https://randomuser.me/api/portraits/med/men/2.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/men/2.jpg"}},{"name":{"title":"miss","first":"meral","last":"türkyılmaz"},"email":"meral.türkyılmaz@example.com","login":{"uuid":"8384f911-e3d6-49f4-8039-07a8702c7be3","username":"silverfrog580","password":"quantum","salt":"BzgitqmT","md5":"4e96afa59de940efd28a9583d87d8b96","sha1":"059d4ce482cc23ffc5ef5b22735cd8d81bacb35e","sha256":"74982f9a8824ef256d2c02d0cd7cad87c9c8b7ca9894afc07b505a7c1f7a7539"},"picture":{"large":"https://randomuser.me/api/portraits/women/77.jpg","medium":"https://randomuser.me/api/portraits/med/women/77.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/77.jpg"}},{"name":{"title":"miss","first":"marilou","last":"robert"},"email":"marilou.robert@example.com","login":{"uuid":"721aebad-3950-4776-b612-543bbc586ec0","username":"lazycat347","password":"jayden","salt":"bWo4VgxG","md5":"278c88c72472cb899459c7c2cbf7be2f","sha1":"25ea373fd3e6a2fac88834726ae8b335096756ab","sha256":"205bbdbe848dca5affbf6573b608fd411f1dee1f8489d889f6e19786e826dc9a"},"picture":{"large":"https://randomuser.me/api/portraits/women/1.jpg","medium":"https://randomuser.me/api/portraits/med/women/1.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/1.jpg"}},{"name":{"title":"mr","first":"chester","last":"nichols"},"email":"chester.nichols@example.com","login":{"uuid":"5489a93e-46d4-4841-8b7d-bd36305f90c3","username":"greenbear930","password":"cindy","salt":"YBogzTBu","md5":"492802aabe2510fba3dc4264755b844e","sha1":"d843474258714c65f8fe60cd6b21ccc37fdcceb0","sha256":"a3ad245d28f67dcdab19065d2eb823394b5ffac290e752e581a1c966b4a0d2b8"},"picture":{"large":"https://randomuser.me/api/portraits/men/68.jpg","medium":"https://randomuser.me/api/portraits/med/men/68.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/men/68.jpg"}},{"name":{"title":"mr","first":"viljami","last":"niva"},"email":"viljami.niva@example.com","login":{"uuid":"15878140-50d8-4814-a344-6cf03405f62a","username":"browncat582","password":"python","salt":"DjcOyDo0","md5":"da922701a288e95829a91c965c37b97b","sha1":"4322c72b3bad51ab0ccf734a46724ea5e6a71852","sha256":"7a384878f5f544aa2c54951d814c2fcdf071dcae8503fa4862a1ddd1c1c212d1"},"picture":{"large":"https://randomuser.me/api/portraits/men/19.jpg","medium":"https://randomuser.me/api/portraits/med/men/19.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/men/19.jpg"}},{"name":{"title":"mr","first":"kevin","last":"burkard"},"email":"kevin.burkard@example.com","login":{"uuid":"5b19d631-6b75-4360-a1db-6eb3e3ad0527","username":"lazysnake631","password":"frederic","salt":"UvgyAQcN","md5":"ae918133f409c49bf97f60e97758bd5e","sha1":"d82f500688c9062c8cd2a99a0a842600de5edd76","sha256":"26cc04ac9a7b4a1de53a2b4173fe8b9758928074cb82d6dcf49ece6295227bd0"},"picture":{"large":"https://randomuser.me/api/portraits/men/43.jpg","medium":"https://randomuser.me/api/portraits/med/men/43.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/men/43.jpg"}},{"name":{"title":"mrs","first":"سارینا","last":"گلشن"},"email":"سارینا.گلشن@example.com","login":{"uuid":"61ef2554-6a50-4dad-b1fa-7a5d63720a67","username":"angryladybug713","password":"pictere","salt":"x64Nakfu","md5":"b8896da577ef0a967cbf3fefbd6e3efc","sha1":"8c4aa61dda8e0e1d02e3200f49e44adfaadb1f2b","sha256":"d726213b8f3283b09249dea2d4728cf17f3a8bfe05a82947eefde9818720776c"},"picture":{"large":"https://randomuser.me/api/portraits/women/60.jpg","medium":"https://randomuser.me/api/portraits/med/women/60.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/60.jpg"}},{"name":{"title":"ms","first":"patricia","last":"elliott"},"email":"patricia.elliott@example.com","login":{"uuid":"b3288035-19b7-4093-b18b-c1c4191ac14c","username":"crazykoala618","password":"juliet","salt":"NSg3uFYH","md5":"2f9f7b649e71eefa1ea9790e178d0e30","sha1":"5f4d14184a4b16d26912efd79ce63ef3dc534ea7","sha256":"7c5987569e9a01e0358d10b930c9cd38f471c20a32964bce79dac697f58ecc7c"},"picture":{"large":"https://randomuser.me/api/portraits/women/30.jpg","medium":"https://randomuser.me/api/portraits/med/women/30.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/30.jpg"}},{"name":{"title":"mr","first":"ulrico","last":"das neves"},"email":"ulrico.dasneves@example.com","login":{"uuid":"5166a0a5-7d9e-4467-974e-c2361ac0d904","username":"silverpanda228","password":"anna","salt":"rSGTDfV4","md5":"76bdea7e75ad83a604b6f660f8d4dba0","sha1":"5a8e6bc0f7c394b11124aa11fca2c12eb08daf04","sha256":"ee7e55626fef2dd4265195b298254a7953e4b4b2641f7ee2508cd977e811190d"},"picture":{"large":"https://randomuser.me/api/portraits/men/69.jpg","medium":"https://randomuser.me/api/portraits/med/men/69.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/men/69.jpg"}},{"name":{"title":"mrs","first":"hilla","last":"leppo"},"email":"hilla.leppo@example.com","login":{"uuid":"e7ea0938-df6e-4ea0-a15b-07a6596a9153","username":"bigmouse688","password":"melody","salt":"Wm8U4ZdG","md5":"5f585a79c5820f1842ae114e96efb4fb","sha1":"4280ca2ea3f1b18712877dde12924d4caf1ab470","sha256":"fe82aebcabc4c56ebea6d937a9a56341d6eb35d75bbfe78e623bcf118435d1b2"},"picture":{"large":"https://randomuser.me/api/portraits/women/88.jpg","medium":"https://randomuser.me/api/portraits/med/women/88.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/88.jpg"}},{"name":{"title":"mr","first":"zander","last":"goedhart"},"email":"zander.goedhart@example.com","login":{"uuid":"12760591-6b2a-4bc3-a6ea-426ab1e58d87","username":"yellowladybug794","password":"wolf359","salt":"q0nb4His","md5":"995ad42f04b826c3b40cb59fe1de5969","sha1":"b1fec50018e4198e80859941dbeb4b5dea453a08","sha256":"16b574ec87ea102e0293044ee124f7ef5848885375ae5b288f816df23a0b3630"},"picture":{"large":"https://randomuser.me/api/portraits/men/51.jpg","medium":"https://randomuser.me/api/portraits/med/men/51.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/men/51.jpg"}},{"name":{"title":"mrs","first":"lois","last":"allen"},"email":"lois.allen@example.com","login":{"uuid":"91f4c92b-f983-4fe1-becd-644456415e5e","username":"ticklishswan357","password":"bombay","salt":"li7TnEmp","md5":"92abb0952d8d7c01c0072ecd9af53edf","sha1":"0617f7c224fdf6c7902146d2c6c7e0479a6a6cf7","sha256":"da02b627980298bf51e0bb073a343a427cadc554af46246fc8c9ef0013df6b9a"},"picture":{"large":"https://randomuser.me/api/portraits/women/62.jpg","medium":"https://randomuser.me/api/portraits/med/women/62.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/62.jpg"}},{"name":{"title":"miss","first":"sarah","last":"côté"},"email":"sarah.côté@example.com","login":{"uuid":"8a54b8b7-1ce1-4cb2-96be-8dd89b4eafad","username":"happybear796","password":"real","salt":"2dMEb92y","md5":"c2d306277dff157f1160cfda7f605144","sha1":"4b6d7702986efb5e06424817b8d26fbfd4536ddd","sha256":"80c418971d16af1dfdad4a8ca30b43da4cf1411ae2c95e3f62b40b22904bc7b1"},"picture":{"large":"https://randomuser.me/api/portraits/women/4.jpg","medium":"https://randomuser.me/api/portraits/med/women/4.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/4.jpg"}},{"name":{"title":"madame","first":"cäcilia","last":"bernard"},"email":"cäcilia.bernard@example.com","login":{"uuid":"2f00f684-86ca-440e-9c3f-ced08919a05e","username":"lazytiger110","password":"1005","salt":"jzc5Bsrq","md5":"7b7d051af7406eeeefc60d9f2ef97a64","sha1":"60a06a73d9f9592b3feff55a5cf5cd504e1d5beb","sha256":"c5bc2d472f98f4793a436aa48087d33f57fe9be88f33669a7ebc9e52d22ad829"},"picture":{"large":"https://randomuser.me/api/portraits/women/86.jpg","medium":"https://randomuser.me/api/portraits/med/women/86.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/86.jpg"}},{"name":{"title":"mr","first":"clifton","last":"butler"},"email":"clifton.butler@example.com","login":{"uuid":"999e01d8-7d03-4db6-b3f1-f01f4f30e824","username":"blueduck736","password":"soleil","salt":"u7ZlgeHQ","md5":"1b7c2f5e7f44ae99abde85338fa4f162","sha1":"90b968c97e96b16d15ef4131bec792f0d3b376aa","sha256":"525c4a705edb3dae7c6070ceeb12d2880827dcaad4b8294b4956afd843e114f2"},"picture":{"large":"https://randomuser.me/api/portraits/men/76.jpg","medium":"https://randomuser.me/api/portraits/med/men/76.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/men/76.jpg"}},{"name":{"title":"mr","first":"nathan","last":"jean-baptiste"},"email":"nathan.jean-baptiste@example.com","login":{"uuid":"7cc388d7-1d21-44e5-98f6-c307ad5fe41d","username":"whitebear777","password":"tree","salt":"pzryGa6S","md5":"cf76b0069ff3854146f4c9c06fe486a4","sha1":"6247988bd605be0aaea7abf51568bacf3ec8ee0f","sha256":"cffbd44ffe108a850486dd51a9d22b004d39ca4c4df61b83a2cdd6d457388a75"},"picture":{"large":"https://randomuser.me/api/portraits/men/10.jpg","medium":"https://randomuser.me/api/portraits/med/men/10.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/men/10.jpg"}},{"name":{"title":"mr","first":"cory","last":"fuller"},"email":"cory.fuller@example.com","login":{"uuid":"0a907de4-91e2-42fb-b733-2dd4d2a98439","username":"blackpanda805","password":"ssssss","salt":"iNsNCYdN","md5":"8d7b86797949e44eff390eeab7cf83fe","sha1":"2d98fd4c0c3169f1c472d38762a3a184c19b73b2","sha256":"7f9b0e9b70959f2b0bc3e78be8437468868f7b19c76f8c0e4fa923d0c341714d"},"picture":{"large":"https://randomuser.me/api/portraits/men/73.jpg","medium":"https://randomuser.me/api/portraits/med/men/73.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/men/73.jpg"}},{"name":{"title":"mr","first":"eelis","last":"wiitala"},"email":"eelis.wiitala@example.com","login":{"uuid":"89dd6793-43e0-4672-a22a-2de31363c86d","username":"orangezebra920","password":"12345a","salt":"6iAKG5Io","md5":"ad21e685bbc6fa3853696b69c1e4f671","sha1":"bfc1d83cca74d223b1a30a7a7aacab1edc9eb2e4","sha256":"19655c4147e17baec523ed0e5910a7cdafff9d05d967a26f95a1e7fde46e7fc3"},"picture":{"large":"https://randomuser.me/api/portraits/men/96.jpg","medium":"https://randomuser.me/api/portraits/med/men/96.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/men/96.jpg"}},{"name":{"title":"miss","first":"aldalgisa","last":"rezende"},"email":"aldalgisa.rezende@example.com","login":{"uuid":"70fd6b60-c210-4f26-9fa0-6062e14949bc","username":"lazymeercat553","password":"paranoid","salt":"hFOZRN8a","md5":"abd30d705e66c7eab8418ca996c5144b","sha1":"f22079b73df8f677828cceda0c64bbb20359f95c","sha256":"18f2f2760ac71eb7675d6a739d2f4ce8ca4ebb70d235ea3d1261f6a290e7457f"},"picture":{"large":"https://randomuser.me/api/portraits/women/94.jpg","medium":"https://randomuser.me/api/portraits/med/women/94.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/94.jpg"}},{"name":{"title":"mr","first":"donald","last":"harter"},"email":"donald.harter@example.com","login":{"uuid":"b20c73c4-9826-40e7-8440-7e028e888ec5","username":"sadswan850","password":"zhuai","salt":"9vhHzbmn","md5":"20398a769132b70a307af5cdd51bfa16","sha1":"87c69d2c980b8b87a261b541b392809e52388127","sha256":"5c94a52306cf8846ddeb945361e649be12c250f65fb7e85b276edeb5bf086ca7"},"picture":{"large":"https://randomuser.me/api/portraits/men/28.jpg","medium":"https://randomuser.me/api/portraits/med/men/28.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/men/28.jpg"}},{"name":{"title":"ms","first":"ملینا","last":"علیزاده"},"email":"ملینا.علیزاده@example.com","login":{"uuid":"c7ae195d-077c-42d7-a1df-d57e8521194e","username":"silverrabbit267","password":"krista","salt":"CplrtbOD","md5":"30a49dcd0b3edd0316c073ad4aaf53a4","sha1":"e37abd743455b391e280bd65f5e054e1bc20e4bf","sha256":"aed7aa1b2c5a103226cd754dacdf1a985dfb707b755aa0e97b127aba1f1e3adc"},"picture":{"large":"https://randomuser.me/api/portraits/women/26.jpg","medium":"https://randomuser.me/api/portraits/med/women/26.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/26.jpg"}},{"name":{"title":"miss","first":"emily","last":"jones"},"email":"emily.jones@example.com","login":{"uuid":"5fd2118a-d33b-482c-a50f-1e5e26a68184","username":"happywolf134","password":"ussy","salt":"37c0X3xS","md5":"34f3d9e1e14e9feb0e92d9a709c62563","sha1":"459b88ae75c2c0af9f78adf9bfda862659c89102","sha256":"241741eed613236cb73ba959c793aeaac77fcb4c754787239e7e67d5ae10ea89"},"picture":{"large":"https://randomuser.me/api/portraits/women/67.jpg","medium":"https://randomuser.me/api/portraits/med/women/67.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/67.jpg"}},{"name":{"title":"mr","first":"enrique","last":"campos"},"email":"enrique.campos@example.com","login":{"uuid":"464680eb-ee0c-4be1-8402-94bfa9024e0b","username":"bluezebra528","password":"unreal","salt":"xYRyTOtW","md5":"52ca4cbb4c389f59b7acb2b19d6df8fc","sha1":"77c6272f9ac7084fbbacd0c3e11e0948e670266d","sha256":"e86d89a4d5a199e3e0ab002a1f693921d99cb6f3f52974cc8f4c62d9d4e9e476"},"picture":{"large":"https://randomuser.me/api/portraits/men/59.jpg","medium":"https://randomuser.me/api/portraits/med/men/59.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/men/59.jpg"}},{"name":{"title":"miss","first":"آدرینا","last":"رضاییان"},"email":"آدرینا.رضاییان@example.com","login":{"uuid":"eb0434b8-9c3b-42a4-a7d0-ad2bcafbbe40","username":"blackleopard583","password":"jimbo1","salt":"NZQ9Omny","md5":"0c6c599907a034e7061b31464959afba","sha1":"f1ca7da1acd9fe717f8f7fb5ced7840d6347cf54","sha256":"a44bd8c8327aa49318b59f7f9e2b84a2ec1a5f50144b5ca9d963f83a072fbadf"},"picture":{"large":"https://randomuser.me/api/portraits/women/78.jpg","medium":"https://randomuser.me/api/portraits/med/women/78.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/78.jpg"}},{"name":{"title":"ms","first":"sophia","last":"barnes"},"email":"sophia.barnes@example.com","login":{"uuid":"c0d20718-e5fd-473c-9814-7772f66e934e","username":"bluefrog900","password":"massive","salt":"QxwI7ISj","md5":"09df03a432ea6f0011ed084c3c11f12e","sha1":"8f8fa342dc1228712cebaa765c090315267aa85c","sha256":"07b4d2dc8b58727844491d49206d6be34f3b7c2de14220c440a17ebeb4bc6982"},"picture":{"large":"https://randomuser.me/api/portraits/women/87.jpg","medium":"https://randomuser.me/api/portraits/med/women/87.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/87.jpg"}},{"name":{"title":"miss","first":"meral","last":"mayhoş"},"email":"meral.mayhoş@example.com","login":{"uuid":"98e8308f-e3df-4a43-8624-cb3704831c0d","username":"silvermeercat233","password":"hallo","salt":"xM4DknR2","md5":"f05ab9e15e4c5e701c033615f6569f87","sha1":"1df8f0a4d7932b4c0c574a970ee8ef0b7b6f9c91","sha256":"4632401aee5b46ef382829c0eca75d839d4ac1d74b53b6e0a30a274b76c0f58b"},"picture":{"large":"https://randomuser.me/api/portraits/women/59.jpg","medium":"https://randomuser.me/api/portraits/med/women/59.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/59.jpg"}},{"name":{"title":"monsieur","first":"stefano","last":"boyer"},"email":"stefano.boyer@example.com","login":{"uuid":"84d4a18a-dbaf-40a7-9815-fc44bf89c37f","username":"bigzebra429","password":"delete","salt":"puoKDnHi","md5":"3417ac8b3b164c0948f44bc4ca0d3983","sha1":"aebbe789893f9650f50f2375772f8c8761eea933","sha256":"4029336a556246065baea9f1d3541b048dda30365bc4f66823468b378204db18"},"picture":{"large":"https://randomuser.me/api/portraits/men/87.jpg","medium":"https://randomuser.me/api/portraits/med/men/87.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/men/87.jpg"}},{"name":{"title":"miss","first":"hannah","last":"douglas"},"email":"hannah.douglas@example.com","login":{"uuid":"68d565a3-7345-42bd-bcf5-286033fbddf1","username":"yellowdog763","password":"bricks","salt":"Nmk6YKjA","md5":"727c1083d37d0cba72dad37dc617b14d","sha1":"dddaad1a1c200914479b8458540e98521c5f20d7","sha256":"b54cd12bfee7fccc987bfeed693a3720b36ecefada27514df9744b07b460094c"},"picture":{"large":"https://randomuser.me/api/portraits/women/41.jpg","medium":"https://randomuser.me/api/portraits/med/women/41.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/41.jpg"}},{"name":{"title":"ms","first":"alexandra","last":"williams"},"email":"alexandra.williams@example.com","login":{"uuid":"69782c2d-d121-468e-bdae-b7c52a4c1742","username":"whiteelephant591","password":"indiana","salt":"ATOlZalM","md5":"622894b29da3aac05f9984f3a64708d8","sha1":"8f7cf3ca8df6d8c5b6b75cfa66cf9f9d6d6f5e02","sha256":"43513ad2ad561bf677ba2e9b6a1dc8cb36f0eaf15a8f37cd6f0242363c7c6900"},"picture":{"large":"https://randomuser.me/api/portraits/women/69.jpg","medium":"https://randomuser.me/api/portraits/med/women/69.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/69.jpg"}},{"name":{"title":"ms","first":"فاطمه زهرا","last":"یاسمی"},"email":"فاطمه زهرا.یاسمی@example.com","login":{"uuid":"0a78bc29-c464-4476-9fae-d77ac1401628","username":"beautifulfrog975","password":"1992","salt":"ousVusvv","md5":"bb1302c95a23197902d9f3bbee0ec176","sha1":"01404615f673437f02bdcdc4865273e34a3c9c6f","sha256":"78adb608348919cbabdf76d22a4b7060169880b9da06c9481401fec25a84e389"},"picture":{"large":"https://randomuser.me/api/portraits/women/70.jpg","medium":"https://randomuser.me/api/portraits/med/women/70.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/70.jpg"}},{"name":{"title":"mr","first":"antonio","last":"larson"},"email":"antonio.larson@example.com","login":{"uuid":"1bbfba8e-f0fc-4e6b-a6f0-ceb052c166b2","username":"whitedog676","password":"zzzz","salt":"ZWfKE72n","md5":"5c1c03744eda47666973d09199926786","sha1":"469bbadcefca565a1ef8b6530f81dd48d4bcc9fd","sha256":"ff663d21e27ac3a87c3dbf046f9f251adc4ca22734b5761a9acd1d183aba5e42"},"picture":{"large":"https://randomuser.me/api/portraits/men/24.jpg","medium":"https://randomuser.me/api/portraits/med/men/24.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/men/24.jpg"}},{"name":{"title":"mr","first":"fred","last":"caldeira"},"email":"fred.caldeira@example.com","login":{"uuid":"bbd23c94-9624-41e6-9885-6db6ba4ce3f7","username":"brownelephant690","password":"venus","salt":"DhGqfR1E","md5":"b57785ad290082a3ab30d9c4bf5e3d4f","sha1":"3cb31992a85b062f43a65655420fc754cefeb378","sha256":"4c77feb2a2159663c95fb07504afedfa2bf32190e53a7ffe90928274519b5935"},"picture":{"large":"https://randomuser.me/api/portraits/men/67.jpg","medium":"https://randomuser.me/api/portraits/med/men/67.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/men/67.jpg"}},{"name":{"title":"miss","first":"harper","last":"chan"},"email":"harper.chan@example.com","login":{"uuid":"981c93a9-1c1a-460f-9d41-82c9e8fba034","username":"redswan377","password":"daniel","salt":"GPexXQva","md5":"1ea6c5b7622227f567b155ee5835b21f","sha1":"d6c49ac857c5db63d9eb90d6c7257e6afe96d8d0","sha256":"0c0c7e11bbd7a15228e568e644a08d9e3ebf126e27f93ed59406e3b86b470f28"},"picture":{"large":"https://randomuser.me/api/portraits/women/57.jpg","medium":"https://randomuser.me/api/portraits/med/women/57.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/57.jpg"}},{"name":{"title":"mr","first":"isaac","last":"foster"},"email":"isaac.foster@example.com","login":{"uuid":"9865adaf-a336-4728-888c-1f524c956513","username":"orangepeacock396","password":"gong","salt":"iKGXr2FO","md5":"2b455580a28ff776f879117006afee7c","sha1":"297a7427e9ef1cfae92c273e20a3c374a19a9058","sha256":"f0c703c920b2dada4b3b6e3d9c15d48231a4e13232169582ddc3e320c2c24992"},"picture":{"large":"https://randomuser.me/api/portraits/men/75.jpg","medium":"https://randomuser.me/api/portraits/med/men/75.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/men/75.jpg"}},{"name":{"title":"ms","first":"sofia","last":"thompson"},"email":"sofia.thompson@example.com","login":{"uuid":"164899fd-0dac-4e16-8a65-8f8b2935e75e","username":"ticklishcat371","password":"sanchez","salt":"NuHYPfd7","md5":"1d056f0b2e41af22a0e6df8555e19feb","sha1":"7dea3ad1816bc3dcdbbbc615e82df3df1c5b8126","sha256":"0eb64944c3ff5630566e5fb2398f632bb47d5630132ea4a9dfc7f484808d4b81"},"picture":{"large":"https://randomuser.me/api/portraits/women/13.jpg","medium":"https://randomuser.me/api/portraits/med/women/13.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/13.jpg"}},{"name":{"title":"mr","first":"nikolai","last":"leknes"},"email":"nikolai.leknes@example.com","login":{"uuid":"1e1336e8-a928-4e7d-9469-e723bd6c9255","username":"crazykoala177","password":"paloma","salt":"tLTwbk7t","md5":"e41c699ad136c4ea11d033b0b06cf156","sha1":"db365d715cec13a6a967210ac9d3d29c058a23a3","sha256":"cd4df19df0362a2b2b3558fd57d59ab81453cfae79818a1bbc43dfe7a1f257c2"},"picture":{"large":"https://randomuser.me/api/portraits/men/24.jpg","medium":"https://randomuser.me/api/portraits/med/men/24.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/men/24.jpg"}},{"name":{"title":"mrs","first":"tracey","last":"holmes"},"email":"tracey.holmes@example.com","login":{"uuid":"5148d91c-93ae-458b-ad27-32e14d02db35","username":"silverdog290","password":"antares","salt":"qJDRb9Nx","md5":"d84ef7f85df02ec6071d9cb3db7364b7","sha1":"4d3b20101fcfffb297e33bdce265a492d8809f2b","sha256":"2c3db1a7cba3ab3648416a3d6176bced6fb2a1c30baba934a01bc3df9ecef430"},"picture":{"large":"https://randomuser.me/api/portraits/women/34.jpg","medium":"https://randomuser.me/api/portraits/med/women/34.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/34.jpg"}},{"name":{"title":"mr","first":"jackson","last":"graves"},"email":"jackson.graves@example.com","login":{"uuid":"738241f5-daeb-40f2-99d2-0019fe464be0","username":"angryrabbit821","password":"dalshe","salt":"TBhjQVCF","md5":"a823184a2c96d4ef9cad2e1f6cd76bec","sha1":"1704b0c951cd8dd5e6a329e3094d52cd437b6895","sha256":"9b4390de4b9b168517e44a0fbfa05828ed9de851a9d042d37d1fcdd486b2cfba"},"picture":{"large":"https://randomuser.me/api/portraits/men/61.jpg","medium":"https://randomuser.me/api/portraits/med/men/61.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/men/61.jpg"}},{"name":{"title":"mr","first":"marius","last":"christensen"},"email":"marius.christensen@example.com","login":{"uuid":"96d216e8-f863-441d-8c6d-a32ed9262f3b","username":"angryfrog846","password":"laser","salt":"aKXKPrDg","md5":"ad587f10e0892b7cee7bdc1ee4d06af5","sha1":"c439ea274ba828f651dda2a77500626c56f5c882","sha256":"baa68279d6583381d087de87e7a3ccf8fe2fab27e24d96a73bfd07113ba5617f"},"picture":{"large":"https://randomuser.me/api/portraits/men/74.jpg","medium":"https://randomuser.me/api/portraits/med/men/74.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/men/74.jpg"}},{"name":{"title":"ms","first":"nina","last":"patel"},"email":"nina.patel@example.com","login":{"uuid":"6d972a93-22f0-46d1-b509-20a147e0013d","username":"whiteduck288","password":"heather","salt":"VCgq0jQi","md5":"b6ecf553e67fcfe93a9689dbab37f2d6","sha1":"84b819747c4936ab6381e0b8ae2412cdaad9f82d","sha256":"60800e5ae1296661d2ecd863afe66656de047488f1e47c043187eebf7c4d7cd7"},"picture":{"large":"https://randomuser.me/api/portraits/women/92.jpg","medium":"https://randomuser.me/api/portraits/med/women/92.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/92.jpg"}},{"name":{"title":"mrs","first":"concepcion","last":"pascual"},"email":"concepcion.pascual@example.com","login":{"uuid":"ee2be2a3-6ba2-4d02-a7f3-880b7f48a976","username":"blackzebra423","password":"1125","salt":"QDWorRwz","md5":"b74fa463bfa308e6aca2f96c76b34bcb","sha1":"a7529ead23de93e5f2db687d67d6e7c5d02beaea","sha256":"c182bada63ae9ab234d4ac7cdaa510bc9ec22510c3a46a67f89ce35ef8fd5405"},"picture":{"large":"https://randomuser.me/api/portraits/women/92.jpg","medium":"https://randomuser.me/api/portraits/med/women/92.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/92.jpg"}},{"name":{"title":"ms","first":"cindy","last":"nøttveit"},"email":"cindy.nøttveit@example.com","login":{"uuid":"bd946a10-b1af-49fc-aa67-f9354c545501","username":"tinymouse617","password":"houdini","salt":"V7GO43hb","md5":"0e033277de44eefa77c138333ce513ec","sha1":"1fd152363a64c81491ac094c16c2a3aadf1fd6b9","sha256":"867955797ca3e7242e1867b2c19f4a3d1b36ae0d0ba14691378a53d96b9959f7"},"picture":{"large":"https://randomuser.me/api/portraits/women/64.jpg","medium":"https://randomuser.me/api/portraits/med/women/64.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/64.jpg"}},{"name":{"title":"ms","first":"elizabeth","last":"kumar"},"email":"elizabeth.kumar@example.com","login":{"uuid":"06bea9e7-4e88-4708-8ce7-75197084b93c","username":"lazyfrog236","password":"aaaaa","salt":"VArBUBHV","md5":"45689e5f08b9af1c0ca94247405608ed","sha1":"8ee8584c10f73d5be828bda0d82987da3a09beb7","sha256":"55a02d1bb5724eda4f8128b81b24822d9d644c4f40adfb57c6d6105c1641196e"},"picture":{"large":"https://randomuser.me/api/portraits/women/58.jpg","medium":"https://randomuser.me/api/portraits/med/women/58.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/58.jpg"}},{"name":{"title":"mr","first":"gregorio","last":"delgado"},"email":"gregorio.delgado@example.com","login":{"uuid":"c197fd3f-f2a4-4132-b3d2-f031561695d5","username":"purpledog715","password":"dragonfl","salt":"i7WAt58I","md5":"ba52b7d11a89e7130d1e882913395e3c","sha1":"2b8c273ba76fbbc88d6aea1915f8f5807e399f89","sha256":"6a38c7e04ce2a0c016cd494cf43259d184041b41b76494550d18f4fc2e1369bb"},"picture":{"large":"https://randomuser.me/api/portraits/men/29.jpg","medium":"https://randomuser.me/api/portraits/med/men/29.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/men/29.jpg"}},{"name":{"title":"mr","first":"alvaro","last":"pascual"},"email":"alvaro.pascual@example.com","login":{"uuid":"1841bd7f-33aa-47a2-a216-fa8b3e2457e5","username":"purplewolf201","password":"fisting","salt":"rrDcYSnv","md5":"f119fdcda5028c8f2ceb141bec5aeef6","sha1":"996bcd2f32023cb34db36ea443786efe63b04e1d","sha256":"1ae0364690c4ae648b5bd4ed90747d682382b889a4c05284685e6096cd420592"},"picture":{"large":"https://randomuser.me/api/portraits/men/98.jpg","medium":"https://randomuser.me/api/portraits/med/men/98.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/men/98.jpg"}},{"name":{"title":"miss","first":"eva","last":"hayes"},"email":"eva.hayes@example.com","login":{"uuid":"02f82159-3377-48a1-aca5-04ad45d9c197","username":"bluebear683","password":"ppppppp","salt":"aGjlbuoG","md5":"9874c4e9493129e4b4914afc74764a99","sha1":"d5f3ac81a72e6f478b60e751ff3125a3f396bc95","sha256":"fbb0b5ac1b1c88c66ecfa5f39e2b38d2f8ebdc0a10e78c0750a3f5d3f71f4c78"},"picture":{"large":"https://randomuser.me/api/portraits/women/11.jpg","medium":"https://randomuser.me/api/portraits/med/women/11.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/11.jpg"}},{"name":{"title":"mr","first":"johnni","last":"jones"},"email":"johnni.jones@example.com","login":{"uuid":"2ec60fa6-5412-40b0-b64e-afc4c298c4d8","username":"redpeacock665","password":"sweet1","salt":"igZDvEQX","md5":"6b41c8cd7f6054d777f2f71db8332d93","sha1":"70e68e69b184a1adb8f8734430b84a1e55ae9ec4","sha256":"dcc4b38195737601a9a78995cdee8516ab2a41ae5f974c9c06ad8dd5746862ec"},"picture":{"large":"https://randomuser.me/api/portraits/men/89.jpg","medium":"https://randomuser.me/api/portraits/med/men/89.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/men/89.jpg"}},{"name":{"title":"mrs","first":"caroline","last":"smith"},"email":"caroline.smith@example.com","login":{"uuid":"8b502cf5-155f-45c1-bcc9-8117f67e7cc8","username":"bluedog539","password":"cheshire","salt":"7ii0R8Ng","md5":"25d82a82fa1ed6892b202e0682f82317","sha1":"e5f4568e1a202d3a74fd660bb63ea107c8ccd904","sha256":"2cae18bd38cd0e5c49112de4cfe925bafff1b2412446ed29f9acb8777eb82851"},"picture":{"large":"https://randomuser.me/api/portraits/women/35.jpg","medium":"https://randomuser.me/api/portraits/med/women/35.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/35.jpg"}},{"name":{"title":"ms","first":"verena","last":"horn"},"email":"verena.horn@example.com","login":{"uuid":"eecbaa95-63a5-48ca-90fc-d242f22f4b43","username":"crazypanda949","password":"storms","salt":"3b6NziLl","md5":"9263958c4821dc384ed2b90b2f3c67d3","sha1":"ccc9a64eabda962c3e74e3f594a8ba1eb2e7ac93","sha256":"778a315c53c6225b4766cbe8699b211a543ca29d5b127decf8cfb2af1a9b63a1"},"picture":{"large":"https://randomuser.me/api/portraits/women/25.jpg","medium":"https://randomuser.me/api/portraits/med/women/25.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/25.jpg"}},{"name":{"title":"miss","first":"kayla","last":"stevens"},"email":"kayla.stevens@example.com","login":{"uuid":"a9ef18d0-01c7-4d3b-b05e-cea59b502f4a","username":"ticklishpanda218","password":"moon","salt":"0HLG8lDP","md5":"2c4d40a5bf8f63d672dbb1102e41663b","sha1":"74f8b2d85237052204c6e501f0f4db7d0d217ad8","sha256":"b71098fd55836cc6a9d9b1a4cd74af6eec219b2f5dbd34968f3a2f557f67cf00"},"picture":{"large":"https://randomuser.me/api/portraits/women/38.jpg","medium":"https://randomuser.me/api/portraits/med/women/38.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/38.jpg"}},{"name":{"title":"mr","first":"xavier","last":"cortes"},"email":"xavier.cortes@example.com","login":{"uuid":"10639023-90be-407b-9b63-189f677bae84","username":"goldengorilla488","password":"liverpoo","salt":"J3scYmKz","md5":"068a1ca96322f18aa924a1576b191cb1","sha1":"28a940d16558d51e5390716b0eadd6229df792a5","sha256":"36ab3eee756130e9c7192c583d09b2f2f0ec6f04feea871facb03be909a59c0f"},"picture":{"large":"https://randomuser.me/api/portraits/men/69.jpg","medium":"https://randomuser.me/api/portraits/med/men/69.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/men/69.jpg"}},{"name":{"title":"ms","first":"savannah","last":"davies"},"email":"savannah.davies@example.com","login":{"uuid":"7b0fa51c-23da-4548-b4b2-369175ac0218","username":"crazycat341","password":"aileen","salt":"CJbyVN8i","md5":"2a7814f7f9bdfe570ad86af2d0caccb0","sha1":"870b92117196511c2d78ce46d564cba19e1a5c49","sha256":"1aac52f581f3c574a9c700ed936df2f97c077c4cfe7d7a3e0b4d4b8c56688dbf"},"picture":{"large":"https://randomuser.me/api/portraits/women/74.jpg","medium":"https://randomuser.me/api/portraits/med/women/74.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/74.jpg"}},{"name":{"title":"mr","first":"jurrit","last":"capelle"},"email":"jurrit.capelle@example.com","login":{"uuid":"12ac7840-ccb2-494c-a224-fade1d720687","username":"brownlion277","password":"thirteen","salt":"IO1z28Sg","md5":"e474f42d3304cd797ae8191fd5b9ed67","sha1":"10364947780c1e953e7936d50a2fd14289e3d669","sha256":"a79406976f637cd83b110cf3222daf1d03e6f9419059a337d2cb3a9ec3ed68d2"},"picture":{"large":"https://randomuser.me/api/portraits/men/66.jpg","medium":"https://randomuser.me/api/portraits/med/men/66.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/men/66.jpg"}},{"name":{"title":"miss","first":"ülkü","last":"kılıççı"},"email":"ülkü.kılıççı@example.com","login":{"uuid":"02579065-7830-409c-9a47-1805d85b225f","username":"lazygoose386","password":"ethan","salt":"0G1mB0w5","md5":"68fd25d6320a4b37fb33071330574719","sha1":"bc3dc2e2a3bf436bab820e63bf567b5842a8a08b","sha256":"53bf659edfcc4f389ad2a906400bd72c7affb36f5d769f3da3c9d11dd97379b7"},"picture":{"large":"https://randomuser.me/api/portraits/women/3.jpg","medium":"https://randomuser.me/api/portraits/med/women/3.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/3.jpg"}},{"name":{"title":"miss","first":"sharon","last":"thomas"},"email":"sharon.thomas@example.com","login":{"uuid":"718da955-a8e4-4586-a22e-0dfb9ff637de","username":"lazyostrich831","password":"laurel","salt":"SyeexUXM","md5":"2783a46b4da2e7e1a296f35f41ae26dc","sha1":"5169de6361385c1fd062cb57e2c472d55d5688be","sha256":"c0d34643fce7af6cde3b11f4c44beb4e3556a8b1839ef605da98788d147abe21"},"picture":{"large":"https://randomuser.me/api/portraits/women/92.jpg","medium":"https://randomuser.me/api/portraits/med/women/92.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/92.jpg"}},{"name":{"title":"mrs","first":"debbie","last":"daniels"},"email":"debbie.daniels@example.com","login":{"uuid":"14f2b26e-3324-4fa0-b358-d9e23a7f3719","username":"ticklishswan834","password":"wind","salt":"1hd9mX4F","md5":"90b7dffc5ce25121b6e399e06c57e855","sha1":"1e1737a67f8beb9c81205c4fafebd317115fb8ce","sha256":"29529b04d39b47f6f430262e5ce94146a0cf5d4fbe3c8a6a566d3aca181e0948"},"picture":{"large":"https://randomuser.me/api/portraits/women/56.jpg","medium":"https://randomuser.me/api/portraits/med/women/56.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/56.jpg"}},{"name":{"title":"mr","first":"dominic","last":"martin"},"email":"dominic.martin@example.com","login":{"uuid":"6b3b8408-f3ea-47c3-8b56-02f5e1f11aa7","username":"yellowbear221","password":"alberto","salt":"te2ngxa3","md5":"acce687879dcf7595c1b0c39da3fb022","sha1":"f5dd8ec4c3a4355b73a1b1a5f3335c9f54217540","sha256":"15cef79870a09e04ec1945fd43f77e463c05eca91e2f8b30f8008a3960ee4c22"},"picture":{"large":"https://randomuser.me/api/portraits/men/11.jpg","medium":"https://randomuser.me/api/portraits/med/men/11.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/men/11.jpg"}},{"name":{"title":"monsieur","first":"naser","last":"fabre"},"email":"naser.fabre@example.com","login":{"uuid":"1356d393-221f-41d2-962d-d39a4547d4ce","username":"sadbird589","password":"lonestar","salt":"cQ4VmdEu","md5":"ff561eab10aff2f5b584c0558657ab7f","sha1":"3d118825e901b7f8eb9035fbcacfd1ff1c0d7b73","sha256":"63fd4dd68d3f7cc9b1e8dd53b483d3968d7c54769190b4dab5ddbbe575203ed7"},"picture":{"large":"https://randomuser.me/api/portraits/men/44.jpg","medium":"https://randomuser.me/api/portraits/med/men/44.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/men/44.jpg"}},{"name":{"title":"mrs","first":"kira","last":"hopf"},"email":"kira.hopf@example.com","login":{"uuid":"ee207ea9-eaa8-40cc-be62-bda104a5db36","username":"yellowbear248","password":"senator","salt":"WPP5XHWp","md5":"f4347acb3841180413e0cabb3aa28d05","sha1":"7398dc00bcd7afb236894e109cc5327e8537b0e9","sha256":"c775faab14adc363aeffa8d0bc0e1eec0467a93776a7712ee809d0febaa71133"},"picture":{"large":"https://randomuser.me/api/portraits/women/10.jpg","medium":"https://randomuser.me/api/portraits/med/women/10.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/10.jpg"}},{"name":{"title":"ms","first":"liliane","last":"klemm"},"email":"liliane.klemm@example.com","login":{"uuid":"4b9acc64-7c78-4c07-82d8-965c554aadc1","username":"yellowswan335","password":"lucy","salt":"uYDVEAKi","md5":"8a6f1426b071f3869afe65e8dafb152f","sha1":"8543a50eb6cfbcce65f2cf0c8090163539b35e21","sha256":"8212a36ce11476fd97044078d77276a36761a7adf6249775674167bdbf9e5f42"},"picture":{"large":"https://randomuser.me/api/portraits/women/82.jpg","medium":"https://randomuser.me/api/portraits/med/women/82.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/82.jpg"}},{"name":{"title":"mr","first":"theodore","last":"kim"},"email":"theodore.kim@example.com","login":{"uuid":"cc9bf6f5-363c-4e25-932f-c65ef8492aa7","username":"greenkoala434","password":"audia4","salt":"i1HMoRJL","md5":"dde5ffa27ddd82845a4f081e59ae2019","sha1":"7ca393add714aa39c05aff0d9d0d7cd28119e114","sha256":"15e1af7fc7de8e4758bd91f8b0bcef23b26a257be9c9e7f223a0eaafde571fe3"},"picture":{"large":"https://randomuser.me/api/portraits/men/82.jpg","medium":"https://randomuser.me/api/portraits/med/men/82.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/men/82.jpg"}},{"name":{"title":"mr","first":"محمد","last":"کامروا"},"email":"محمد.کامروا@example.com","login":{"uuid":"3536ac82-c0d8-4c54-9c33-ceefe611b55b","username":"yellowkoala951","password":"freak","salt":"4Ml3ef6t","md5":"052f37bb6fd028cacc4fa85dcbfd1155","sha1":"028416132a8d4413bd3b2591bda8249cf94b85c0","sha256":"4feeb4bf269a29cf6652c620096f4924c03cb668317f59f4039f75f89d563a13"},"picture":{"large":"https://randomuser.me/api/portraits/men/19.jpg","medium":"https://randomuser.me/api/portraits/med/men/19.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/men/19.jpg"}},{"name":{"title":"mr","first":"jayden","last":"walker"},"email":"jayden.walker@example.com","login":{"uuid":"84470cb3-55af-4127-b71b-39dd88cf0194","username":"crazytiger311","password":"something","salt":"SLKJOHUo","md5":"0d1b1b152105851d3c3c5c5331f9cfb3","sha1":"7c98e96f1e8bb2d8804d919af4b3a319c9feb76c","sha256":"d06bab57e630972c1b15b7d3c4ff042a7f3a463d63c53ec3d593c08746896440"},"picture":{"large":"https://randomuser.me/api/portraits/men/34.jpg","medium":"https://randomuser.me/api/portraits/med/men/34.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/men/34.jpg"}},{"name":{"title":"mr","first":"anthony","last":"wheeler"},"email":"anthony.wheeler@example.com","login":{"uuid":"5b18a0fb-8a56-4592-9b17-08230538b30a","username":"silverfrog934","password":"peng","salt":"cQz3nvy5","md5":"98914f9fe879aa324d06522ca445138d","sha1":"bdd6edd8539f2013b88b8b6ec1233412d8f26fd6","sha256":"63977a60f6b660e62a9136e9f4c9042a226aed7366f2dd547bb56b9ade1a33b8"},"picture":{"large":"https://randomuser.me/api/portraits/men/57.jpg","medium":"https://randomuser.me/api/portraits/med/men/57.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/men/57.jpg"}},{"name":{"title":"miss","first":"ستایش","last":"زارعی"},"email":"ستایش.زارعی@example.com","login":{"uuid":"18b53a96-6058-4725-8ca4-21d757aceaba","username":"bluegorilla154","password":"monaco","salt":"xFLMqLVp","md5":"eb0e8ff02580c138256b43ae77974d70","sha1":"0d04873ddb1166151aa5a7cc8af69775940ec5c8","sha256":"b3ca0847570fb235b4a41ebdbcf316d30603da94ec1fcdbfa605226cc477f579"},"picture":{"large":"https://randomuser.me/api/portraits/women/24.jpg","medium":"https://randomuser.me/api/portraits/med/women/24.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/24.jpg"}},{"name":{"title":"miss","first":"alexandra","last":"hawkins"},"email":"alexandra.hawkins@example.com","login":{"uuid":"ec2fdfc4-8b76-461d-bc8a-773b32d4983d","username":"greenswan139","password":"beanie","salt":"GxFqIuK9","md5":"7d06241ea7e6979a9c6b9a12419c27fe","sha1":"488cb1da90c2f691a596ab0315ae1d6f4f8114ff","sha256":"dc2f5787783c57a7527c6e9ea3c22d06ec78815a22ff2e9d6366e5aba1010cf0"},"picture":{"large":"https://randomuser.me/api/portraits/women/64.jpg","medium":"https://randomuser.me/api/portraits/med/women/64.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/64.jpg"}},{"name":{"title":"mr","first":"noah","last":"edwards"},"email":"noah.edwards@example.com","login":{"uuid":"3ad37f22-fbfa-4133-a749-53a090986359","username":"heavymouse816","password":"finance","salt":"2iQIXWYp","md5":"16e4d99de26c2bd740e729a4d0a3cba5","sha1":"5adbb2fc7cac880813edd4047ed94b0b460e817d","sha256":"44c8180836fe034f3cade7578f625758c14b447ddd33d49b1978c37f5c19346b"},"picture":{"large":"https://randomuser.me/api/portraits/men/15.jpg","medium":"https://randomuser.me/api/portraits/med/men/15.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/men/15.jpg"}},{"name":{"title":"mr","first":"eibert","last":"schaaf"},"email":"eibert.schaaf@example.com","login":{"uuid":"dc7e66f6-64af-45b6-a6a8-6e0ea6293d4b","username":"brownsnake844","password":"alibaba","salt":"DGOlbZIt","md5":"48458a9857802e8c3d1f7975f6105012","sha1":"c0a414931699903682e2baf0605cfe06097fb309","sha256":"1c33093010473fc1500450d736aa82249e4473a87fd8b38a33cc67501ca16af6"},"picture":{"large":"https://randomuser.me/api/portraits/men/49.jpg","medium":"https://randomuser.me/api/portraits/med/men/49.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/men/49.jpg"}},{"name":{"title":"miss","first":"constance","last":"riviere"},"email":"constance.riviere@example.com","login":{"uuid":"d48fe60e-e875-4e73-a63a-a898d223eaeb","username":"crazyrabbit636","password":"pornos","salt":"hmd7OSOs","md5":"a185f11fd95db3b0ec9d2240e72afa41","sha1":"5ee8401eb89e192c997c4a65d5cddea56a661f1c","sha256":"fd4ce34a102d6f9c554a35cdd9d1f9398587f3d4e5a8b3d94f58f8723c7d60fd"},"picture":{"large":"https://randomuser.me/api/portraits/women/46.jpg","medium":"https://randomuser.me/api/portraits/med/women/46.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/46.jpg"}},{"name":{"title":"ms","first":"miriam","last":"vicente"},"email":"miriam.vicente@example.com","login":{"uuid":"98f27a9d-b24a-464e-af0c-c650f239766b","username":"tinylion981","password":"dominion","salt":"8fh6e0gF","md5":"3ef64eb210867f54b9c73c5ccd120851","sha1":"585d80537f01933d8071c83ad9debe47f991f04e","sha256":"d8981520044bb5f638bc4fc11c03b693f6a7d15072a3583b7c38bde8a8a8d682"},"picture":{"large":"https://randomuser.me/api/portraits/women/84.jpg","medium":"https://randomuser.me/api/portraits/med/women/84.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/84.jpg"}},{"name":{"title":"mr","first":"bela","last":"lüdecke"},"email":"bela.lüdecke@example.com","login":{"uuid":"40d39685-66f0-4bd0-9cb9-95faf73e8998","username":"orangelion444","password":"78945612","salt":"R31kaTT0","md5":"d57adc1c67cdde77179c72340f459334","sha1":"8ba734be70a156d7b881157ada70dd0f2e7e80e1","sha256":"f3aa552a86d5232c1dbdced7a47b53f416513ad063bbb8a30aea37dc3f535f72"},"picture":{"large":"https://randomuser.me/api/portraits/men/16.jpg","medium":"https://randomuser.me/api/portraits/med/men/16.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/men/16.jpg"}},{"name":{"title":"mrs","first":"lillian","last":"curtis"},"email":"lillian.curtis@example.com","login":{"uuid":"2fef6d8e-2b7f-4c20-9790-6caf21414c6c","username":"blacktiger155","password":"shao","salt":"JJXrGZqr","md5":"53c852caca0a7c24d9fef0191c2aa021","sha1":"4b27f5183ff6861c378e61af9ee6ad917714faa8","sha256":"2c62cd815deefdc9d3c7776ceae97194cd41f7a1a8b1218fadea718abe3653fd"},"picture":{"large":"https://randomuser.me/api/portraits/women/76.jpg","medium":"https://randomuser.me/api/portraits/med/women/76.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/76.jpg"}},{"name":{"title":"ms","first":"sam","last":"fitzpatrick"},"email":"sam.fitzpatrick@example.com","login":{"uuid":"dee4569f-65fb-439c-9acb-3711123bd8ad","username":"yellowmeercat398","password":"stealth","salt":"MybtUc4X","md5":"33060d697549a9b17a110addd1ffb1b4","sha1":"b81c8f2a0b40449b619c590fb5af2f8aedfae42c","sha256":"21f335f969a1e64beae9181f58f050eec6d406c78c01057908101218bcbcf7b2"},"picture":{"large":"https://randomuser.me/api/portraits/women/10.jpg","medium":"https://randomuser.me/api/portraits/med/women/10.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/10.jpg"}},{"name":{"title":"ms","first":"hailey","last":"carpenter"},"email":"hailey.carpenter@example.com","login":{"uuid":"fb1e3935-4a73-423c-bf64-7e8ed168fe3a","username":"goldengorilla897","password":"video1","salt":"ggJRVRTj","md5":"be81df2e0fbb5eb62a0edda9cb946a7f","sha1":"ae50c6f7efe2db2cc712df2b7382ca530991e965","sha256":"697d2dbeb6d3d2449039461fc171b8ff461d0e05038bcde3512f6f54899cb567"},"picture":{"large":"https://randomuser.me/api/portraits/women/30.jpg","medium":"https://randomuser.me/api/portraits/med/women/30.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/30.jpg"}},{"name":{"title":"mr","first":"viljami","last":"wuori"},"email":"viljami.wuori@example.com","login":{"uuid":"75270eee-82a4-4c9f-bf8d-37f5e5755df0","username":"whiteostrich282","password":"bugs","salt":"t4TnBwYv","md5":"41d9f799f79fcfdb477d8f68047cbb02","sha1":"b89aef7877109649e5d5f53e052ba9244eb512c0","sha256":"a864d896676795595edf34a478573b5f3811f8f6562783d79d2331cbf2568488"},"picture":{"large":"https://randomuser.me/api/portraits/men/57.jpg","medium":"https://randomuser.me/api/portraits/med/men/57.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/men/57.jpg"}},{"name":{"title":"mrs","first":"caitlin","last":"chambers"},"email":"caitlin.chambers@example.com","login":{"uuid":"22c93189-ad6e-4c8f-8c39-bde14d5643b6","username":"yellowleopard411","password":"balloon","salt":"0N0IoRTz","md5":"35edee8194455ede2ceddc872d159609","sha1":"2dfb4270ee3ef92dd278aeb1b03efc12a930795e","sha256":"3f98bffd3277609a020c5130fb090f7b89707a2bc978e1de179a0103d12c9034"},"picture":{"large":"https://randomuser.me/api/portraits/women/93.jpg","medium":"https://randomuser.me/api/portraits/med/women/93.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/93.jpg"}},{"name":{"title":"ms","first":"mercedes","last":"martinez"},"email":"mercedes.martinez@example.com","login":{"uuid":"6cc988ad-acf4-46fc-bba5-a8438ad3ec9d","username":"crazyduck374","password":"manning","salt":"TcAor7S0","md5":"9fe6e5208340b3391c69beb8c7160fc5","sha1":"e4073dae01c107031b0bcf07e1e5c8d483f02f0f","sha256":"f22d149560d81eeaf41ded114a1c165598da9ced50714ab4d5bcdd5dfe7bcb15"},"picture":{"large":"https://randomuser.me/api/portraits/women/35.jpg","medium":"https://randomuser.me/api/portraits/med/women/35.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/35.jpg"}},{"name":{"title":"miss","first":"eva","last":"larson"},"email":"eva.larson@example.com","login":{"uuid":"f3ce8fde-d3f9-41ec-ad70-2af7f9c9c0fb","username":"yellowwolf176","password":"speed1","salt":"dg3OpoOZ","md5":"9d59c69ea139dd6b90e8d78205b16dc9","sha1":"e4547ef0bb4c4be29c48378ddd84ac4b3a2f48f6","sha256":"a38ad61fa80994ab0bab3ae588cb0bf269f8355b4cf12732d642bc155827c84d"},"picture":{"large":"https://randomuser.me/api/portraits/women/62.jpg","medium":"https://randomuser.me/api/portraits/med/women/62.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/62.jpg"}},{"name":{"title":"mrs","first":"mia","last":"flores"},"email":"mia.flores@example.com","login":{"uuid":"c214aa34-c945-41d3-960e-1f02e11890b6","username":"goldenelephant281","password":"demon","salt":"JyMaRNt7","md5":"30d70ea81161db8a233a63aad2ddeb9c","sha1":"28fd0c33f93046136fb282f4d14b6cf3958360fa","sha256":"8ddd87f2941ef775cec1d61b57dd87e0370d107e423519e62534de1247cd0f0a"},"picture":{"large":"https://randomuser.me/api/portraits/women/77.jpg","medium":"https://randomuser.me/api/portraits/med/women/77.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/77.jpg"}},{"name":{"title":"mr","first":"ali","last":"kulaksızoğlu"},"email":"ali.kulaksızoğlu@example.com","login":{"uuid":"5a140dd6-ed6e-4627-b48f-63090bec3075","username":"goldenfrog744","password":"harley","salt":"OfBrYunA","md5":"fec77079fcb5057f2026cd7a78654623","sha1":"1eeb7a7054f7be7486e785f48c5096d378541e14","sha256":"72580221aa70a01856cfab86a43bb6a56e300a787164d04884a1ea7e643ea7df"},"picture":{"large":"https://randomuser.me/api/portraits/men/71.jpg","medium":"https://randomuser.me/api/portraits/med/men/71.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/men/71.jpg"}},{"name":{"title":"ms","first":"esther","last":"reyes"},"email":"esther.reyes@example.com","login":{"uuid":"08357ccc-8ed0-4688-9605-d1dca56dd29a","username":"happybird979","password":"merlin1","salt":"EzM0ibwM","md5":"ebe68e983af6c1ce9c272eb956b8c72c","sha1":"a96f7bbf6fbe056a806bc423f5e367372455c814","sha256":"3729087cfd24b5b00437f51a78dbf938316478a06fd87b468f45f652bc60e6f7"},"picture":{"large":"https://randomuser.me/api/portraits/women/48.jpg","medium":"https://randomuser.me/api/portraits/med/women/48.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/48.jpg"}},{"name":{"title":"mr","first":"phillip","last":"blaschke"},"email":"phillip.blaschke@example.com","login":{"uuid":"7d3d856d-bb8a-4dca-aec8-c4cf024b1749","username":"smalldog795","password":"heritage","salt":"tlR2ZTqg","md5":"607c6740c88901db2126907e0b57becb","sha1":"5b8a7d9c7894e396e0c3f2f6480a73c90b57c4b8","sha256":"899c87f16fc868bdd9291749da139efdcd95f2492b27c9ae0020d3e799f3c3ee"},"picture":{"large":"https://randomuser.me/api/portraits/men/60.jpg","medium":"https://randomuser.me/api/portraits/med/men/60.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/men/60.jpg"}},{"name":{"title":"miss","first":"kristin","last":"kuhn"},"email":"kristin.kuhn@example.com","login":{"uuid":"fc9a0529-6d71-4d4a-97c7-4c4515d7c769","username":"sadelephant857","password":"benny","salt":"cQwGIcEY","md5":"927bb232fba5484cceb852e5e1ad28e9","sha1":"d984a0675b928a47d3f9ad4b9191ee28168ca1bb","sha256":"30e61b971a6a3618172bfb58e6123d944e7312fe0751578af97a2f341f481977"},"picture":{"large":"https://randomuser.me/api/portraits/women/16.jpg","medium":"https://randomuser.me/api/portraits/med/women/16.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/16.jpg"}},{"name":{"title":"ms","first":"esperanza","last":"mora"},"email":"esperanza.mora@example.com","login":{"uuid":"fd4c6d5b-4afb-4736-a507-3399af1e70e4","username":"angrygoose641","password":"umpire","salt":"s8UhluD5","md5":"0ffe9e771045b2641a5ba8bd2436b86f","sha1":"78ce8625ee4b51af92d3f2b30e3f5a13cafbeb79","sha256":"39db9545d1176ff0a1959fddd60473f9f17cba02b85fc61e6e2943ed86b1dc0e"},"picture":{"large":"https://randomuser.me/api/portraits/women/16.jpg","medium":"https://randomuser.me/api/portraits/med/women/16.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/16.jpg"}},{"name":{"title":"mr","first":"luis","last":"nordhaug"},"email":"luis.nordhaug@example.com","login":{"uuid":"b2d893b9-9166-4589-9c2a-0de5e4a715b4","username":"heavyfrog429","password":"marie1","salt":"cyFmmXwE","md5":"6549fa572d1dabf8d9890207515e6a6c","sha1":"270b8e6d30947a003254d136d664502dec3449f3","sha256":"38dd3386c6bcf61fb9c0424d802eafb58f2537f3a57d9af51f66075cbdba975c"},"picture":{"large":"https://randomuser.me/api/portraits/men/55.jpg","medium":"https://randomuser.me/api/portraits/med/men/55.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/men/55.jpg"}},{"name":{"title":"mr","first":"alexis","last":"denis"},"email":"alexis.denis@example.com","login":{"uuid":"b5c7c89e-c90b-4a2b-9ab2-5bbe93d11e55","username":"bluegorilla242","password":"danger","salt":"JmuYLM2l","md5":"8be187c184451ab644b5675a88772895","sha1":"5d73aae5b543a113955d714e24a3c831e834689e","sha256":"9a17b7aca1ad6d8f8678294504638f820749a077df2774be03facf6d6412a28f"},"picture":{"large":"https://randomuser.me/api/portraits/men/10.jpg","medium":"https://randomuser.me/api/portraits/med/men/10.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/men/10.jpg"}},{"name":{"title":"miss","first":"marie","last":"macrae"},"email":"marie.macrae@example.com","login":{"uuid":"fd230b81-9ad6-4ec0-b1dc-8622d78ce575","username":"redlion691","password":"triple","salt":"431EdvB7","md5":"b2b7d63c7797d80ce3a53c0f7038ce77","sha1":"1e509667ceb4b51854b6c3bbda3c82c57cc48584","sha256":"38eedb233bc6f7a108b9e4c4226ed37432c12e21771941c25b448f945b19730d"},"picture":{"large":"https://randomuser.me/api/portraits/women/7.jpg","medium":"https://randomuser.me/api/portraits/med/women/7.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/7.jpg"}},{"name":{"title":"ms","first":"paige","last":"nguyen"},"email":"paige.nguyen@example.com","login":{"uuid":"b999a8b3-d4e3-48f5-a9dc-f3cdc5106457","username":"heavytiger412","password":"streaming","salt":"wPvrz4S0","md5":"401a92ea30c40d4a53fdf933e2365a75","sha1":"aa67e838c2b01513c40d4857df9785846ac0b1ac","sha256":"faf906005ae0a59557925195885c5d66242dec3748604c3b1a01a79f2e8589bd"},"picture":{"large":"https://randomuser.me/api/portraits/women/31.jpg","medium":"https://randomuser.me/api/portraits/med/women/31.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/31.jpg"}},{"name":{"title":"monsieur","first":"konrad","last":"vidal"},"email":"konrad.vidal@example.com","login":{"uuid":"8aa10f9b-543a-40a6-a356-eab5ede9a424","username":"blackdog852","password":"&","salt":"Lqh0dIwm","md5":"3afc09417321159685f445c51486711f","sha1":"7c2749db3a9b615c84767a3be344dc1657840bd1","sha256":"e34bc693a5d40ced55a5b74ea48ad5b3aa236075013b7cb783cbd7d2bcd35d11"},"picture":{"large":"https://randomuser.me/api/portraits/men/22.jpg","medium":"https://randomuser.me/api/portraits/med/men/22.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/men/22.jpg"}},{"name":{"title":"ms","first":"teresa","last":"roos"},"email":"teresa.roos@example.com","login":{"uuid":"84724897-0ce0-4afc-8b97-40e5c1526899","username":"redfrog604","password":"cocacola","salt":"7a4veRkD","md5":"6104d2d6f68d3bb95365c0b13059e60a","sha1":"29ab2c83e676fe2f00ae536f59f8aa5cb53882b0","sha256":"de0ec09c7943009b335dbb7ce3132834f8bad1e24909d2a337f1f0e9100c7466"},"picture":{"large":"https://randomuser.me/api/portraits/women/51.jpg","medium":"https://randomuser.me/api/portraits/med/women/51.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/51.jpg"}},{"name":{"title":"ms","first":"michelle","last":"barrett"},"email":"michelle.barrett@example.com","login":{"uuid":"c49360d6-43ea-4eba-8f6e-cc82e84b18b0","username":"greenkoala681","password":"homepage","salt":"0qzXg2FI","md5":"2b7f0f81223dbb119b73820f743837e0","sha1":"eb67c95185f767de4be0fa8b6efc30dd8242a5fb","sha256":"81ea70f260c74e5fea59949543febba93c90261b412ae8ddcf33e0a5e63e49d1"},"picture":{"large":"https://randomuser.me/api/portraits/women/38.jpg","medium":"https://randomuser.me/api/portraits/med/women/38.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/38.jpg"}},{"name":{"title":"monsieur","first":"federico","last":"jean"},"email":"federico.jean@example.com","login":{"uuid":"b719e985-8485-49f1-9d01-4a8fe670e4d5","username":"bigwolf931","password":"callum","salt":"KXE78cwE","md5":"afed8ab1d3fa2d190506b0f33eb02683","sha1":"a12601e89082d93a3ff5c9369ad0006d0cc9302f","sha256":"39f61acf887bf2459b9ed53dfa69e71fc5d65e86ff8ab9c41560f24178dc188a"},"picture":{"large":"https://randomuser.me/api/portraits/men/28.jpg","medium":"https://randomuser.me/api/portraits/med/men/28.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/men/28.jpg"}},{"name":{"title":"mr","first":"valgi","last":"farias"},"email":"valgi.farias@example.com","login":{"uuid":"c73159d2-b8cc-4d35-bac0-e29587ea446c","username":"purpledog502","password":"diane","salt":"ub75AxP7","md5":"4e936450ec36b5089c41d50c69e8fbcb","sha1":"a25186692aa0396b9a5031bd0dd422d91b4680e7","sha256":"de5f3fc662c1eb0bbca7768908db110f1ed44253a4384c00081211bc6310a86d"},"picture":{"large":"https://randomuser.me/api/portraits/men/32.jpg","medium":"https://randomuser.me/api/portraits/med/men/32.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/men/32.jpg"}},{"name":{"title":"miss","first":"courtney","last":"moreno"},"email":"courtney.moreno@example.com","login":{"uuid":"7a6b4c0e-b639-4fc4-b997-8a20d8413dd7","username":"organictiger677","password":"bearbear","salt":"kIWv5Ecj","md5":"cd5b35d0875baba409eed885d59433c3","sha1":"384d5c565e7cde813d6bf5edf60860ea48dfc0e2","sha256":"98946f0c2527f860cf0ed00db1ee0b1a619ef43c56742ac42e1388b34c34b62e"},"picture":{"large":"https://randomuser.me/api/portraits/women/62.jpg","medium":"https://randomuser.me/api/portraits/med/women/62.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/62.jpg"}},{"name":{"title":"miss","first":"rose","last":"payne"},"email":"rose.payne@example.com","login":{"uuid":"f83f0a2e-903c-466f-bca7-4b0e4aef47a3","username":"happybear216","password":"brazil","salt":"8j9wRXlr","md5":"03338a8886e3a76479224c6668d5c232","sha1":"8918d3844fc1fb973695076d5e09838b07eac959","sha256":"8ba711c0bb0b4645a8e2c720e8b2f38878bd17042f38fdeb60a0315fedcd6b2d"},"picture":{"large":"https://randomuser.me/api/portraits/women/17.jpg","medium":"https://randomuser.me/api/portraits/med/women/17.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/17.jpg"}},{"name":{"title":"mrs","first":"becky","last":"perry"},"email":"becky.perry@example.com","login":{"uuid":"6075f413-0fab-4a4b-8c95-571e2169d5e7","username":"lazypanda556","password":"viagra","salt":"u2wXGpbG","md5":"12707644e836d4c6f4cd9249fc9ce62e","sha1":"04201b44bf04868f1dc0df3706310deb72d34d1f","sha256":"220018521493aa61fcab2bbb690f1258dc38715e6a1b0a01e1fcfcd8a7ca8671"},"picture":{"large":"https://randomuser.me/api/portraits/women/43.jpg","medium":"https://randomuser.me/api/portraits/med/women/43.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/43.jpg"}},{"name":{"title":"mr","first":"quaresma","last":"barbosa"},"email":"quaresma.barbosa@example.com","login":{"uuid":"5eabf6da-fc1a-46a4-bf12-e1f184e9f36e","username":"blackpeacock637","password":"encore","salt":"d2Ywp0xQ","md5":"332a4c1956351d0f67f35f1b2c67fc59","sha1":"64046c3edec35b170c687856c04acd5d65784413","sha256":"ae665891249339261a4774bcfc09f1188c412dc7838f11ed63fb03de375f57f0"},"picture":{"large":"https://randomuser.me/api/portraits/men/39.jpg","medium":"https://randomuser.me/api/portraits/med/men/39.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/men/39.jpg"}},{"name":{"title":"ms","first":"ivy","last":"edwards"},"email":"ivy.edwards@example.com","login":{"uuid":"60927eb1-ee2b-4e4d-8ff1-04f94518a250","username":"ticklishzebra616","password":"emilio","salt":"o3R2V3wp","md5":"a83507706b5a29ed6c08623361facee8","sha1":"e3f33bcbdd2965c7ca35ba6ca0644463239e2049","sha256":"7b6d8c4b6b538e9764cf387b7eac2505d02f61ab50f38edef315dbaa8d7bb859"},"picture":{"large":"https://randomuser.me/api/portraits/women/11.jpg","medium":"https://randomuser.me/api/portraits/med/women/11.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/11.jpg"}}] diff --git a/tests/_utils/asyncserver/index.js b/tests/_utils/asyncserver/index.js new file mode 100644 index 0000000..5da7c05 --- /dev/null +++ b/tests/_utils/asyncserver/index.js @@ -0,0 +1,73 @@ +/** + * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/* eslint-env node */ + +const http = require( 'http' ); +const fs = require( 'fs' ); +const querystring = require( 'querystring' ); +const url = require( 'url' ); +const { upperFirst } = require( 'lodash' ); + +const hostname = '127.0.0.1'; +const port = 3000; + +const server = http.createServer( function( req, res ) { + res.statusCode = 200; + res.setHeader( 'Content-Type', 'application/json' ); + + const { search } = querystring.parse( url.parse( req.url ).query.toLowerCase() ); + + readEntries( getTimeout() ) + .then( entries => entries + .map( ( { picture, name, login } ) => ( { + id: `@${ login.username }`, + username: login.username, + fullName: `${ upperFirst( name.first ) } ${ upperFirst( name.last ) }`, + thumbnail: picture.thumbnail + } ) ) + .sort( ( a, b ) => a.username.localeCompare( b.username ) ) + .filter( entry => entry.fullName.toLowerCase().includes( search ) || entry.username.toLowerCase().includes( search ) ) + .slice( 0, 10 ) + ) + .then( entries => { + res.setHeader( 'Access-Control-Allow-Origin', '*' ); + res.setHeader( 'Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept' ); + + res.end( JSON.stringify( entries ) + '\n' ); + } ); +} ); + +server.listen( port, hostname, () => { + console.log( `server running at http://${ hostname }:${ port }/` ); +} ); + +function readEntries( timeOut ) { + return new Promise( ( resolve, reject ) => { + fs.readFile( './data/db.json', ( err, data ) => { + if ( err ) { + reject( err ); + } + + const entries = JSON.parse( data ); + + setTimeout( () => { + resolve( entries ); + }, timeOut ); + } ); + } ); +} + +function getTimeout() { + const type = parseInt( Math.random() * 10 ); + + // 60% of requests completes in 150ms. + if ( type < 6 ) { + return 150; + } + + // 40% of requests completes in 400ms, 1s, 2s or 4s. + return [ 400, 1000, 2000, 4000 ][ ( Math.random() * 3 ).toFixed( 0 ) ]; +} diff --git a/tests/manual/mention-asynchronous.html b/tests/manual/mention-asynchronous.html new file mode 100644 index 0000000..8312c15 --- /dev/null +++ b/tests/manual/mention-asynchronous.html @@ -0,0 +1,52 @@ + + + + +

+ +

+ +
+

Hello @

+ +
+ +
CKEditor logo - caption
+
+
+ + diff --git a/tests/manual/mention-asynchronous.js b/tests/manual/mention-asynchronous.js new file mode 100644 index 0000000..7d866ca --- /dev/null +++ b/tests/manual/mention-asynchronous.js @@ -0,0 +1,92 @@ +/** + * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/* global console, window, fetch, document */ + +import global from '@ckeditor/ckeditor5-utils/src/dom/global'; + +import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'; +import Mention from '../../src/mention'; +import Underline from '@ckeditor/ckeditor5-basic-styles/src/underline'; +import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset'; +import Font from '@ckeditor/ckeditor5-font/src/font'; + +ClassicEditor + .create( global.document.querySelector( '#editor' ), { + plugins: [ ArticlePluginSet, Underline, Font, Mention ], + toolbar: [ + 'heading', + '|', 'bulletedList', 'numberedList', 'blockQuote', + '|', 'bold', 'italic', 'underline', 'link', + '|', 'fontFamily', 'fontSize', 'fontColor', 'fontBackgroundColor', + '|', 'insertTable', + '|', 'undo', 'redo' + ], + image: { + toolbar: [ 'imageStyle:full', 'imageStyle:side', '|', 'imageTextAlternative' ] + }, + table: { + contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ], + tableToolbar: [ 'bold', 'italic' ] + }, + mention: { + feeds: [ + { + marker: '@', + feed: getFeed, + itemRenderer: ( { fullName, id, thumbnail } ) => { + const div = document.createElement( 'div' ); + + div.classList.add( 'custom' ); + div.classList.add( 'mention__item' ); + + div.innerHTML = + `` + + '
' + + `${ fullName }` + + `${ id }` + + '
'; + + return div; + } + } + ] + } + } ) + .then( editor => { + window.editor = editor; + } ) + .catch( err => { + console.error( err.stack ); + } ); + +// Simplest cache: +const cache = new Map(); + +function getFeed( text ) { + const useCache = document.querySelector( '#cache-control' ).checked; + + if ( useCache && cache.has( text ) ) { + console.log( `Loading from cache for: "${ text }".` ); + + return cache.get( text ); + } + + const fetchOptions = { + method: 'get', + mode: 'cors' + }; + + return fetch( `http://localhost:3000?search=${ text }`, fetchOptions ) + .then( response => { + const feedItems = response.json(); + + if ( useCache ) { + cache.set( text, feedItems ); + } + + return feedItems; + } ); +} diff --git a/tests/manual/mention-asynchronous.md b/tests/manual/mention-asynchronous.md new file mode 100644 index 0000000..cb42d9c --- /dev/null +++ b/tests/manual/mention-asynchronous.md @@ -0,0 +1,31 @@ +## Mention asynchronous feeds + +### Configuration + +The feed is asynchronous list that is loaded from server (`@` marker) after random delay: + +- 60% of requests completes in 150ms. +- 40% of requests completes in 400ms, 1s, 2s or 4s. + +In order to run the server go to the `tests/_utils/asyncserver/` and run: + +```sh +node index.js +``` + +### Interaction + +Controlling the cache: + +- You can enable caching mechanism of the tests `getFeed()` callback - if checked it will save results and load them from cache for the same query. +- If cache is disabled then no loading nor saving will be performed. + +### Behavior + +There should be no errors even if request took longer time or came out-of-order. + +If the asyncserver is not running the notification should be shown for failed requests. + +### Disclaimer + +This manual tests uses data generated by [Random User Generator](https://randomuser.me/). diff --git a/tests/manual/mention-custom-renderer.js b/tests/manual/mention-custom-renderer.js index 0bb14cb..482c9ca 100644 --- a/tests/manual/mention-custom-renderer.js +++ b/tests/manual/mention-custom-renderer.js @@ -22,6 +22,10 @@ ClassicEditor '|', 'insertTable', '|', 'undo', 'redo' ], + table: { + contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ], + tableToolbar: [ 'bold', 'italic' ] + }, mention: { feeds: [ { diff --git a/tests/manual/mention-custom-view.js b/tests/manual/mention-custom-view.js index d4e5d2f..24ef2d0 100644 --- a/tests/manual/mention-custom-view.js +++ b/tests/manual/mention-custom-view.js @@ -68,6 +68,10 @@ ClassicEditor '|', 'insertTable', '|', 'undo', 'redo' ], + table: { + contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ], + tableToolbar: [ 'bold', 'italic' ] + }, mention: { feeds: [ { diff --git a/tests/manual/mention.js b/tests/manual/mention.js index fd5a805..dd5b04f 100644 --- a/tests/manual/mention.js +++ b/tests/manual/mention.js @@ -120,6 +120,10 @@ ClassicEditor image: { toolbar: [ 'imageStyle:full', 'imageStyle:side', '|', 'imageTextAlternative' ] }, + table: { + contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ], + tableToolbar: [ 'bold', 'italic' ] + }, mention: { feeds: [ { diff --git a/tests/mentionui.js b/tests/mentionui.js index 2bcce9b..7ee3261 100644 --- a/tests/mentionui.js +++ b/tests/mentionui.js @@ -3,7 +3,7 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ -/* global window, document, setTimeout, Event */ +/* global window, document, setTimeout, Event, console */ import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; @@ -323,6 +323,34 @@ describe( 'MentionUI', () => { } ); } ); + it( 'should update the marker if the selection was moved from one valid position to another', () => { + const spy = sinon.spy(); + + return createClassicTestEditor( staticConfig ) + .then( () => { + setData( model, 'foo @ bar []' ); + + model.change( writer => { + writer.insertText( '@', doc.selection.getFirstPosition() ); + } ); + } ) + .then( waitForDebounce ) + .then( () => { + expect( panelView.isVisible ).to.be.true; + expect( editor.model.markers.has( 'mention' ) ).to.be.true; + } ) + .then( () => { + editor.model.markers.on( 'update', spy ); + + model.change( writer => { + writer.setSelection( doc.getRoot().getChild( 0 ), 5 ); + } ); + + sinon.assert.calledOnce( spy ); + expect( editor.model.markers.has( 'mention' ) ).to.be.true; + } ); + } ); + describe( 'static list with large set of results', () => { const bigList = { marker: '@', @@ -884,20 +912,30 @@ describe( 'MentionUI', () => { } ); describe( 'asynchronous list with custom trigger', () => { + const issuesNumbers = [ '#100', '#101', '#102', '#103' ]; + + let feedCallbackStub, feedCallbackTimeout, feedCallbackCallTimes; + beforeEach( () => { - const issuesNumbers = [ '#100', '#101', '#102', '#103' ]; + feedCallbackTimeout = 20; + feedCallbackCallTimes = 0; + + function feedCallback( feedText ) { + return new Promise( resolve => { + setTimeout( () => { + feedCallbackCallTimes++; + resolve( issuesNumbers.filter( number => number.includes( feedText ) ) ); + }, feedCallbackTimeout ); + } ); + } + + feedCallbackStub = testUtils.sinon.stub().callsFake( feedCallback ); return createClassicTestEditor( { feeds: [ { marker: '#', - feed: feedText => { - return new Promise( resolve => { - setTimeout( () => { - resolve( issuesNumbers.filter( number => number.includes( feedText ) ) ); - }, 20 ); - } ); - } + feed: feedCallbackStub } ] } ); @@ -918,6 +956,33 @@ describe( 'MentionUI', () => { } ); } ); + it( 'should fire requestFeed:response when request feed return a response', () => { + setData( model, 'foo []' ); + const eventSpy = sinon.spy(); + mentionUI.on( 'requestFeed:response', eventSpy ); + + model.change( writer => { + writer.insertText( '#', doc.selection.getFirstPosition() ); + } ); + + return waitForDebounce() + .then( () => { + sinon.assert.calledOnce( eventSpy ); + sinon.assert.calledWithExactly( + eventSpy, + sinon.match.any, + { + feed: issuesNumbers, + marker: '#', + feedText: '' + } + ); + expect( panelView.isVisible ).to.be.true; + expect( editor.model.markers.has( 'mention' ) ).to.be.true; + expect( mentionsView.items ).to.have.length( 4 ); + } ); + } ); + it( 'should show filtered results for matched text', () => { setData( model, 'foo []' ); @@ -979,6 +1044,284 @@ describe( 'MentionUI', () => { .then( waitForDebounce ) .then( () => expect( panelView.isVisible ).to.be.false ); } ); + + it( 'should show panel debounced', () => { + setData( model, 'foo []' ); + + model.change( writer => { + writer.insertText( '#', doc.selection.getFirstPosition() ); + } ); + + sinon.assert.notCalled( feedCallbackStub ); + + return Promise.resolve() + .then( wait( 20 ) ) + .then( () => { + sinon.assert.notCalled( feedCallbackStub ); + + model.change( writer => { + writer.insertText( '1', doc.selection.getFirstPosition() ); + } ); + } ) + .then( wait( 20 ) ) + .then( () => { + sinon.assert.notCalled( feedCallbackStub ); + + model.change( writer => { + writer.insertText( '0', doc.selection.getFirstPosition() ); + } ); + } ) + .then( waitForDebounce ) + .then( () => { + sinon.assert.calledOnce( feedCallbackStub ); + + // Should be called with all typed letters before debounce. + sinon.assert.calledWithExactly( feedCallbackStub, '10' ); + + expect( panelView.isVisible ).to.be.true; + expect( editor.model.markers.has( 'mention' ) ).to.be.true; + expect( mentionsView.items ).to.have.length( 4 ); + } ); + } ); + + it( 'should discard requested feed if they came out of order', () => { + setData( model, 'foo []' ); + + model.change( writer => { + writer.insertText( '#', doc.selection.getFirstPosition() ); + } ); + + sinon.assert.notCalled( feedCallbackStub ); + + const panelShowSpy = sinon.spy( panelView, 'show' ); + + // Increase the response time to extend the debounce time out. + feedCallbackTimeout = 300; + + return Promise.resolve() + .then( wait( 20 ) ) + .then( () => { + sinon.assert.notCalled( feedCallbackStub ); + + model.change( writer => { + writer.insertText( '1', doc.selection.getFirstPosition() ); + } ); + } ) + .then( waitForDebounce ) + .then( () => { + sinon.assert.calledOnce( feedCallbackStub ); + sinon.assert.calledWithExactly( feedCallbackStub, '1' ); + + expect( panelView.isVisible, 'panel is hidden' ).to.be.false; + expect( editor.model.markers.has( 'mention' ), 'marker is inserted' ).to.be.true; + + // Make second callback resolve before first. + feedCallbackTimeout = 50; + + model.change( writer => { + writer.insertText( '0', doc.selection.getFirstPosition() ); + } ); + } ) + .then( wait( 300 ) ) // Wait longer so the longer callback will be resolved. + .then( () => { + sinon.assert.calledTwice( feedCallbackStub ); + sinon.assert.calledWithExactly( feedCallbackStub.getCall( 1 ), '10' ); + sinon.assert.calledOnce( panelShowSpy ); + expect( feedCallbackCallTimes ).to.equal( 2 ); + + expect( panelView.isVisible, 'panel is visible' ).to.be.true; + expect( editor.model.markers.has( 'mention' ), 'marker is inserted' ).to.be.true; + expect( mentionsView.items ).to.have.length( 4 ); + } ); + } ); + + it( 'should fire requestFeed:discarded event when requested feed came out of order', () => { + setData( model, 'foo []' ); + + model.change( writer => { + writer.insertText( '#', doc.selection.getFirstPosition() ); + } ); + + sinon.assert.notCalled( feedCallbackStub ); + + const panelShowSpy = sinon.spy( panelView, 'show' ); + const eventSpy = sinon.spy(); + mentionUI.on( 'requestFeed:discarded', eventSpy ); + + // Increase the response time to extend the debounce time out. + feedCallbackTimeout = 300; + + return Promise.resolve() + .then( wait( 20 ) ) + .then( () => { + sinon.assert.notCalled( feedCallbackStub ); + + model.change( writer => { + writer.insertText( '1', doc.selection.getFirstPosition() ); + } ); + } ) + .then( waitForDebounce ) + .then( () => { + sinon.assert.calledOnce( feedCallbackStub ); + sinon.assert.calledWithExactly( feedCallbackStub, '1' ); + + expect( panelView.isVisible, 'panel is hidden' ).to.be.false; + expect( editor.model.markers.has( 'mention' ), 'marker is inserted' ).to.be.true; + + // Make second callback resolve before first. + feedCallbackTimeout = 50; + + model.change( writer => { + writer.insertText( '0', doc.selection.getFirstPosition() ); + } ); + } ) + .then( wait( 300 ) ) // Wait longer so the longer callback will be resolved. + .then( () => { + sinon.assert.calledTwice( feedCallbackStub ); + sinon.assert.calledWithExactly( feedCallbackStub.getCall( 1 ), '10' ); + sinon.assert.calledOnce( panelShowSpy ); + sinon.assert.calledOnce( eventSpy ); + sinon.assert.calledWithExactly( + eventSpy, + sinon.match.any, + { + feed: issuesNumbers, + marker: '#', + feedText: '1' + } + ); + expect( feedCallbackCallTimes ).to.equal( 2 ); + + expect( panelView.isVisible, 'panel is visible' ).to.be.true; + expect( editor.model.markers.has( 'mention' ), 'marker is inserted' ).to.be.true; + expect( mentionsView.items ).to.have.length( 4 ); + } ); + } ); + + it( 'should discard requested feed if mention UI is hidden', () => { + setData( model, 'foo []' ); + + model.change( writer => { + writer.insertText( '#', doc.selection.getFirstPosition() ); + } ); + + sinon.assert.notCalled( feedCallbackStub ); + + feedCallbackTimeout = 200; + + return Promise.resolve() + .then( waitForDebounce ) + .then( () => { + expect( panelView.isVisible ).to.be.false; // Should be still hidden; + // Should be called with empty string. + sinon.assert.calledWithExactly( feedCallbackStub, '' ); + + model.change( writer => { + writer.setSelection( doc.getRoot().getChild( 0 ), 0 ); + } ); + } ) + .then( waitForDebounce ) + .then( () => { + expect( panelView.isVisible ).to.be.false; + expect( editor.model.markers.has( 'mention' ) ).to.be.false; + } ); + } ); + + it( 'should fire requestFeed:error and log warning if requested feed failed', () => { + setData( model, 'foo []' ); + + feedCallbackStub.returns( Promise.reject( 'Request timeout' ) ); + + const warnSpy = sinon.spy( console, 'warn' ); + const eventSpy = sinon.spy(); + mentionUI.on( 'requestFeed:error', eventSpy ); + + model.change( writer => { + writer.insertText( '#', doc.selection.getFirstPosition() ); + } ); + + return waitForDebounce() + .then( () => { + expect( panelView.isVisible, 'panel is hidden' ).to.be.false; + expect( editor.model.markers.has( 'mention' ), 'there is no marker' ).to.be.false; + + sinon.assert.calledWithExactly( warnSpy, sinon.match( /^mention-feed-callback-error:/ ) ); + sinon.assert.calledOnce( eventSpy ); + } ); + } ); + + it( 'should not fail if marker was removed', () => { + setData( model, 'foo []' ); + const selectFirstMentionSpy = sinon.spy( mentionsView, 'selectFirst' ); + + model.change( writer => { + writer.insertText( '#', doc.selection.getFirstPosition() ); + } ); + + sinon.assert.notCalled( feedCallbackStub ); + + // Increase the response time to extend the debounce time out. + feedCallbackTimeout = 500; + + return Promise.resolve() + .then( waitForDebounce ) + .then( wait( 20 ) ) + .then( () => { + model.change( writer => { + writer.setSelection( doc.getRoot().getChild( 0 ), 2 ); + } ); + } ) + .then( wait( 20 ) ) + .then( () => { + feedCallbackTimeout = 1000; + model.change( writer => { + writer.setSelection( doc.getRoot().getChild( 0 ), 'end' ); + } ); + } ) + .then( wait( 500 ) ) + .then( () => { + expect( panelView.isVisible, 'panel is visible' ).to.be.true; + // If there were any errors this will not get called. + // The errors might come from unhandled promise rejections errors. + sinon.assert.calledOnce( selectFirstMentionSpy ); + } ); + } ); + + it( 'should not show panel if selection was moved during fetching a feed', () => { + setData( model, 'foo [#101] bar' ); + + model.change( writer => { + writer.setAttribute( 'mention', { id: '#101', _uid: 1234 }, doc.selection.getFirstRange() ); + } ); + + // Increase the response time to extend the debounce time out. + feedCallbackTimeout = 300; + + model.change( writer => { + writer.setSelection( doc.getRoot().getChild( 1 ), 0 ); + writer.insertText( '#', doc.selection.getFirstPosition() ); + } ); + + sinon.assert.notCalled( feedCallbackStub ); + + return Promise.resolve() + .then( waitForDebounce ) + .then( () => { + sinon.assert.calledOnce( feedCallbackStub ); + + model.change( writer => { + writer.setSelection( doc.getRoot().getChild( 0 ), 6 ); + } ); + + expect( panelView.isVisible ).to.be.false; + } ) + .then( waitForDebounce ) + .then( wait( 20 ) ) + .then( () => { + expect( panelView.isVisible ).to.be.false; + expect( editor.model.markers.has( 'mention' ) ).to.be.false; + } ); + } ); } ); function testOpeningPunctuationCharacter( character, skip = false ) { @@ -1771,14 +2114,18 @@ describe( 'MentionUI', () => { } ); } - function waitForDebounce() { - return new Promise( resolve => { + function wait( timeout ) { + return () => new Promise( resolve => { setTimeout( () => { resolve(); - }, 50 ); + }, timeout ); } ); } + function waitForDebounce() { + return wait( 180 )(); + } + function fireKeyDownEvent( options ) { const eventInfo = new EventInfo( editingView.document, 'keydown' ); const eventData = new DomEventData( editingView.document, {