1 /* global ajaxurl, tinymce, wpLinkL10n, setUserSetting, wpActiveEditor */
5 var editor, searchTimer, River, Query, correctedURL,
8 isTouch = ( 'ontouchend' in document );
11 return editor.dom.getParent( editor.selection.getNode(), 'a' );
15 timeToTriggerRiver: 150,
16 minRiverAJAXDuration: 200,
17 riverBottomThreshold: 5,
23 inputs.wrap = $('#wp-link-wrap');
24 inputs.dialog = $( '#wp-link' );
25 inputs.backdrop = $( '#wp-link-backdrop' );
26 inputs.submit = $( '#wp-link-submit' );
27 inputs.close = $( '#wp-link-close' );
30 inputs.text = $( '#wp-link-text' );
31 inputs.url = $( '#wp-link-url' );
32 inputs.nonce = $( '#_ajax_linking_nonce' );
33 inputs.openInNewTab = $( '#wp-link-target' );
34 inputs.search = $( '#wp-link-search' );
37 rivers.search = new River( $( '#search-results' ) );
38 rivers.recent = new River( $( '#most-recent-results' ) );
39 rivers.elements = inputs.dialog.find( '.query-results' );
41 // Get search notice text
42 inputs.queryNotice = $( '#query-notice-message' );
43 inputs.queryNoticeTextDefault = inputs.queryNotice.find( '.query-notice-default' );
44 inputs.queryNoticeTextHint = inputs.queryNotice.find( '.query-notice-hint' );
46 // Bind event handlers
47 inputs.dialog.keydown( wpLink.keydown );
48 inputs.dialog.keyup( wpLink.keyup );
49 inputs.submit.click( function( event ) {
50 event.preventDefault();
53 inputs.close.add( inputs.backdrop ).add( '#wp-link-cancel a' ).click( function( event ) {
54 event.preventDefault();
58 $( '#wp-link-search-toggle' ).on( 'click', wpLink.toggleInternalLinking );
60 rivers.elements.on( 'river-select', wpLink.updateFields );
62 // Display 'hint' message when search field or 'query-results' box are focused
63 inputs.search.on( 'focus.wplink', function() {
64 inputs.queryNoticeTextDefault.hide();
65 inputs.queryNoticeTextHint.removeClass( 'screen-reader-text' ).show();
66 } ).on( 'blur.wplink', function() {
67 inputs.queryNoticeTextDefault.show();
68 inputs.queryNoticeTextHint.addClass( 'screen-reader-text' ).hide();
71 inputs.search.on( 'keyup input', function() {
74 window.clearTimeout( searchTimer );
75 searchTimer = window.setTimeout( function() {
76 wpLink.searchInternalLinks.call( self );
80 inputs.url.on( 'paste', function() {
81 setTimeout( wpLink.correctURL, 0 );
84 inputs.url.on( 'blur', wpLink.correctURL );
87 // If URL wasn't corrected last time and doesn't start with http:, https:, ? # or /, prepend http://
88 correctURL: function () {
89 var url = $.trim( inputs.url.val() );
91 if ( url && correctedURL !== url && ! /^(?:[a-z]+:|#|\?|\.|\/)/.test( url ) ) {
92 inputs.url.val( 'http://' + url );
97 open: function( editorId ) {
99 $body = $( document.body );
101 $body.addClass( 'modal-open' );
106 window.wpActiveEditor = editorId;
109 if ( ! window.wpActiveEditor ) {
113 this.textarea = $( '#' + window.wpActiveEditor ).get( 0 );
115 if ( typeof tinymce !== 'undefined' ) {
116 // Make sure the link wrapper is the last element in the body,
117 // or the inline editor toolbar may show above the backdrop.
118 $body.append( inputs.backdrop, inputs.wrap );
120 ed = tinymce.get( wpActiveEditor );
122 if ( ed && ! ed.isHidden() ) {
128 if ( editor && tinymce.isIE ) {
129 editor.windowManager.bookmark = editor.selection.getBookmark();
133 if ( ! wpLink.isMCE() && document.selection ) {
134 this.textarea.focus();
135 this.range = document.selection.createRange();
139 inputs.backdrop.show();
143 $( document ).trigger( 'wplink-open', inputs.wrap );
147 return editor && ! editor.isHidden();
150 refresh: function() {
153 // Refresh rivers (clear links, check visibility)
154 rivers.search.refresh();
155 rivers.recent.refresh();
157 if ( wpLink.isMCE() ) {
160 // For the Text editor the "Link text" field is always shown
161 if ( ! inputs.wrap.hasClass( 'has-text-field' ) ) {
162 inputs.wrap.addClass( 'has-text-field' );
165 if ( document.selection ) {
167 text = document.selection.createRange().text || '';
168 } else if ( typeof this.textarea.selectionStart !== 'undefined' &&
169 ( this.textarea.selectionStart !== this.textarea.selectionEnd ) ) {
171 text = this.textarea.value.substring( this.textarea.selectionStart, this.textarea.selectionEnd ) || '';
174 inputs.text.val( text );
175 wpLink.setDefaultValues();
179 // Close the onscreen keyboard
180 inputs.url.focus().blur();
182 // Focus the URL field and highlight its contents.
183 // If this is moved above the selection changes,
184 // IE will show a flashing cursor over the dialog.
185 inputs.url.focus()[0].select();
188 // Load the most recent results if this is the first time opening the panel.
189 if ( ! rivers.recent.ul.children().length ) {
190 rivers.recent.ajax();
193 correctedURL = inputs.url.val().replace( /^http:\/\//, '' );
196 hasSelectedText: function( linkNode ) {
197 var html = editor.selection.getContent();
199 // Partial html and not a fully selected anchor element
200 if ( /</.test( html ) && ( ! /^<a [^>]+>[^<]+<\/a>$/.test( html ) || html.indexOf('href=') === -1 ) ) {
205 var nodes = linkNode.childNodes, i;
207 if ( nodes.length === 0 ) {
211 for ( i = nodes.length - 1; i >= 0; i-- ) {
212 if ( nodes[i].nodeType != 3 ) {
221 mceRefresh: function() {
223 selectedNode = editor.selection.getNode(),
224 linkNode = editor.dom.getParent( selectedNode, 'a[href]' ),
225 onlyText = this.hasSelectedText( linkNode );
228 text = linkNode.innerText || linkNode.textContent;
229 inputs.url.val( editor.dom.getAttrib( linkNode, 'href' ) );
230 inputs.openInNewTab.prop( 'checked', '_blank' === editor.dom.getAttrib( linkNode, 'target' ) );
231 inputs.submit.val( wpLinkL10n.update );
233 text = editor.selection.getContent({ format: 'text' });
234 this.setDefaultValues();
238 inputs.text.val( text || '' );
239 inputs.wrap.addClass( 'has-text-field' );
241 inputs.text.val( '' );
242 inputs.wrap.removeClass( 'has-text-field' );
247 $( document.body ).removeClass( 'modal-open' );
249 if ( ! wpLink.isMCE() ) {
250 wpLink.textarea.focus();
252 if ( wpLink.range ) {
253 wpLink.range.moveToBookmark( wpLink.range.getBookmark() );
254 wpLink.range.select();
260 inputs.backdrop.hide();
263 correctedURL = false;
265 $( document ).trigger( 'wplink-close', inputs.wrap );
268 getAttrs: function() {
272 href: $.trim( inputs.url.val() ),
273 target: inputs.openInNewTab.prop( 'checked' ) ? '_blank' : ''
277 buildHtml: function(attrs) {
278 var html = '<a href="' + attrs.href + '"';
280 if ( attrs.target ) {
281 html += ' target="' + attrs.target + '"';
288 if ( wpLink.isMCE() ) {
295 htmlUpdate: function() {
296 var attrs, text, html, begin, end, cursor, selection,
297 textarea = wpLink.textarea;
303 attrs = wpLink.getAttrs();
304 text = inputs.text.val();
306 // If there's no href, return.
307 if ( ! attrs.href ) {
311 html = wpLink.buildHtml(attrs);
314 if ( document.selection && wpLink.range ) {
316 // Note: If no text is selected, IE will not place the cursor
317 // inside the closing tag.
319 wpLink.range.text = html + ( text || wpLink.range.text ) + '</a>';
320 wpLink.range.moveToBookmark( wpLink.range.getBookmark() );
321 wpLink.range.select();
324 } else if ( typeof textarea.selectionStart !== 'undefined' ) {
326 begin = textarea.selectionStart;
327 end = textarea.selectionEnd;
328 selection = text || textarea.value.substring( begin, end );
329 html = html + selection + '</a>';
330 cursor = begin + html.length;
332 // If no text is selected, place the cursor inside the closing tag.
333 if ( begin === end && ! selection ) {
338 textarea.value.substring( 0, begin ) +
340 textarea.value.substring( end, textarea.value.length )
343 // Update cursor position
344 textarea.selectionStart = textarea.selectionEnd = cursor;
351 mceUpdate: function() {
352 var attrs = wpLink.getAttrs(),
358 if ( tinymce.isIE ) {
359 editor.selection.moveToBookmark( editor.windowManager.bookmark );
362 if ( ! attrs.href ) {
363 editor.execCommand( 'unlink' );
369 if ( inputs.wrap.hasClass( 'has-text-field' ) ) {
370 text = inputs.text.val() || attrs.href;
375 if ( 'innerText' in link ) {
376 link.innerText = text;
378 link.textContent = text;
382 editor.dom.setAttribs( link, attrs );
385 editor.selection.setNode( editor.dom.create( 'a', attrs, editor.dom.encode( text ) ) );
387 editor.execCommand( 'mceInsertLink', false, attrs );
391 editor.nodeChanged();
394 updateFields: function( e, li ) {
395 inputs.url.val( li.children( '.item-permalink' ).val() );
398 setDefaultValues: function() {
400 emailRegexp = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i,
401 urlRegexp = /^(https?|ftp):\/\/[A-Z0-9.-]+\.[A-Z]{2,4}[^ "]*$/i;
403 if ( this.isMCE() ) {
404 selection = editor.selection.getContent();
405 } else if ( document.selection && wpLink.range ) {
406 selection = wpLink.range.text;
407 } else if ( typeof this.textarea.selectionStart !== 'undefined' ) {
408 selection = this.textarea.value.substring( this.textarea.selectionStart, this.textarea.selectionEnd );
411 if ( selection && emailRegexp.test( selection ) ) {
412 // Selection is email address
413 inputs.url.val( 'mailto:' + selection );
414 } else if ( selection && urlRegexp.test( selection ) ) {
416 inputs.url.val( selection.replace( /&|�?38;/gi, '&' ) );
418 // Set URL to default.
419 inputs.url.val( '' );
422 // Update save prompt.
423 inputs.submit.val( wpLinkL10n.save );
426 searchInternalLinks: function() {
427 var t = $( this ), waiting,
430 if ( search.length > 2 ) {
431 rivers.recent.hide();
432 rivers.search.show();
434 // Don't search if the keypress didn't change the title.
435 if ( wpLink.lastSearch == search )
438 wpLink.lastSearch = search;
439 waiting = t.parent().find( '.spinner' ).addClass( 'is-active' );
441 rivers.search.change( search );
442 rivers.search.ajax( function() {
443 waiting.removeClass( 'is-active' );
446 rivers.search.hide();
447 rivers.recent.show();
452 rivers.search.next();
453 rivers.recent.next();
457 rivers.search.prev();
458 rivers.recent.prev();
461 keydown: function( event ) {
465 if ( 27 === event.keyCode ) {
467 event.stopImmediatePropagation();
469 } else if ( 9 === event.keyCode ) {
470 id = event.target.id;
472 // wp-link-submit must always be the last focusable element in the dialog.
473 // following focusable elements will be skipped on keyboard navigation.
474 if ( id === 'wp-link-submit' && ! event.shiftKey ) {
475 inputs.close.focus();
476 event.preventDefault();
477 } else if ( id === 'wp-link-close' && event.shiftKey ) {
478 inputs.submit.focus();
479 event.preventDefault();
483 // Up Arrow and Down Arrow keys.
484 if ( 38 !== event.keyCode && 40 !== event.keyCode ) {
488 if ( document.activeElement &&
489 ( document.activeElement.id === 'link-title-field' || document.activeElement.id === 'url-field' ) ) {
494 fn = 38 === event.keyCode ? 'prev' : 'next';
495 clearInterval( wpLink.keyInterval );
497 wpLink.keyInterval = setInterval( wpLink[ fn ], wpLink.keySensitivity );
498 event.preventDefault();
501 keyup: function( event ) {
502 // Up Arrow and Down Arrow keys.
503 if ( 38 === event.keyCode || 40 === event.keyCode ) {
504 clearInterval( wpLink.keyInterval );
505 event.preventDefault();
509 delayedCallback: function( func, delay ) {
510 var timeoutTriggered, funcTriggered, funcArgs, funcContext;
515 setTimeout( function() {
517 return func.apply( funcContext, funcArgs );
519 timeoutTriggered = true;
523 if ( timeoutTriggered )
524 return func.apply( this, arguments );
526 funcArgs = arguments;
528 funcTriggered = true;
532 toggleInternalLinking: function( event ) {
533 var visible = inputs.wrap.hasClass( 'search-panel-visible' );
535 inputs.wrap.toggleClass( 'search-panel-visible', ! visible );
536 setUserSetting( 'wplink', visible ? '0' : '1' );
537 inputs[ ! visible ? 'search' : 'url' ].focus();
538 event.preventDefault();
542 River = function( element, search ) {
544 this.element = element;
545 this.ul = element.children( 'ul' );
546 this.contentHeight = element.children( '#link-selector-height' );
547 this.waiting = element.find('.river-waiting');
549 this.change( search );
552 $( '#wp-link .query-results, #wp-link #link-selector' ).scroll( function() {
555 element.on( 'click', 'li', function( event ) {
556 self.select( $( this ), event );
560 $.extend( River.prototype, {
561 refresh: function() {
563 this.visible = this.element.is( ':visible' );
566 if ( ! this.visible ) {
574 this.visible = false;
576 // Selects a list item and triggers the river-select event.
577 select: function( li, event ) {
578 var liHeight, elHeight, liTop, elTop;
580 if ( li.hasClass( 'unselectable' ) || li == this.selected )
584 this.selected = li.addClass( 'selected' );
585 // Make sure the element is visible
586 liHeight = li.outerHeight();
587 elHeight = this.element.height();
588 liTop = li.position().top;
589 elTop = this.element.scrollTop();
591 if ( liTop < 0 ) // Make first visible element
592 this.element.scrollTop( elTop + liTop );
593 else if ( liTop + liHeight > elHeight ) // Make last visible element
594 this.element.scrollTop( elTop + liTop - elHeight + liHeight );
596 // Trigger the river-select event
597 this.element.trigger( 'river-select', [ li, event, this ] );
599 deselect: function() {
601 this.selected.removeClass( 'selected' );
602 this.selected = false;
605 if ( ! this.visible )
609 if ( this.selected ) {
610 to = this.selected.prev( 'li' );
616 if ( ! this.visible )
619 var to = this.selected ? this.selected.next( 'li' ) : $( 'li:not(.unselectable):first', this.element );
623 ajax: function( callback ) {
625 delay = this.query.page == 1 ? 0 : wpLink.minRiverAJAXDuration,
626 response = wpLink.delayedCallback( function( results, params ) {
627 self.process( results, params );
629 callback( results, params );
632 this.query.ajax( response );
634 change: function( search ) {
635 if ( this.query && this._search == search )
638 this._search = search;
639 this.query = new Query( search );
640 this.element.scrollTop( 0 );
642 process: function( results, params ) {
643 var list = '', alt = true, classes = '',
644 firstPage = params.page == 1;
648 list += '<li class="unselectable no-matches-found"><span class="item-title"><em>' +
649 wpLinkL10n.noMatchesFound + '</em></span></li>';
652 $.each( results, function() {
653 classes = alt ? 'alternate' : '';
654 classes += this.title ? '' : ' no-title';
655 list += classes ? '<li class="' + classes + '">' : '<li>';
656 list += '<input type="hidden" class="item-permalink" value="' + this.permalink + '" />';
657 list += '<span class="item-title">';
658 list += this.title ? this.title : wpLinkL10n.noTitle;
659 list += '</span><span class="item-info">' + this.info + '</span></li>';
664 this.ul[ firstPage ? 'html' : 'append' ]( list );
666 maybeLoad: function() {
669 bottom = el.scrollTop() + el.height();
671 if ( ! this.query.ready() || bottom < this.contentHeight.height() - wpLink.riverBottomThreshold )
674 setTimeout(function() {
675 var newTop = el.scrollTop(),
676 newBottom = newTop + el.height();
678 if ( ! self.query.ready() || newBottom < self.contentHeight.height() - wpLink.riverBottomThreshold )
681 self.waiting.addClass( 'is-active' );
682 el.scrollTop( newTop + self.waiting.outerHeight() );
684 self.ajax( function() {
685 self.waiting.removeClass( 'is-active' );
687 }, wpLink.timeToTriggerRiver );
691 Query = function( search ) {
693 this.allLoaded = false;
694 this.querying = false;
695 this.search = search;
698 $.extend( Query.prototype, {
700 return ! ( this.querying || this.allLoaded );
702 ajax: function( callback ) {
705 action : 'wp-link-ajax',
707 '_ajax_linking_nonce' : inputs.nonce.val()
711 query.search = this.search;
713 this.querying = true;
715 $.post( ajaxurl, query, function( r ) {
717 self.querying = false;
718 self.allLoaded = ! r;
719 callback( r, query );
724 $( document ).ready( wpLink.init );