]> scripts.mit.edu Git - autoinstalls/wordpress.git/blob - wp-includes/js/wplink.js
WordPress 4.3.1-scripts
[autoinstalls/wordpress.git] / wp-includes / js / wplink.js
1 /* global ajaxurl, tinymce, wpLinkL10n, setUserSetting, wpActiveEditor */
2 var wpLink;
3
4 ( function( $ ) {
5         var editor, searchTimer, River, Query, correctedURL,
6                 inputs = {},
7                 rivers = {},
8                 isTouch = ( 'ontouchend' in document );
9
10         function getLink() {
11                 return editor.dom.getParent( editor.selection.getNode(), 'a' );
12         }
13
14         wpLink = {
15                 timeToTriggerRiver: 150,
16                 minRiverAJAXDuration: 200,
17                 riverBottomThreshold: 5,
18                 keySensitivity: 100,
19                 lastSearch: '',
20                 textarea: '',
21
22                 init: function() {
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' );
28
29                         // Input
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' );
35
36                         // Build Rivers
37                         rivers.search = new River( $( '#search-results' ) );
38                         rivers.recent = new River( $( '#most-recent-results' ) );
39                         rivers.elements = inputs.dialog.find( '.query-results' );
40
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' );
45
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();
51                                 wpLink.update();
52                         });
53                         inputs.close.add( inputs.backdrop ).add( '#wp-link-cancel a' ).click( function( event ) {
54                                 event.preventDefault();
55                                 wpLink.close();
56                         });
57
58                         $( '#wp-link-search-toggle' ).on( 'click', wpLink.toggleInternalLinking );
59
60                         rivers.elements.on( 'river-select', wpLink.updateFields );
61
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();
69                         } );
70
71                         inputs.search.on( 'keyup input', function() {
72                                 var self = this;
73
74                                 window.clearTimeout( searchTimer );
75                                 searchTimer = window.setTimeout( function() {
76                                         wpLink.searchInternalLinks.call( self );
77                                 }, 500 );
78                         });
79
80                         inputs.url.on( 'paste', function() {
81                                 setTimeout( wpLink.correctURL, 0 );
82                         } );
83
84                         inputs.url.on( 'blur', wpLink.correctURL );
85                 },
86
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() );
90
91                         if ( url && correctedURL !== url && ! /^(?:[a-z]+:|#|\?|\.|\/)/.test( url ) ) {
92                                 inputs.url.val( 'http://' + url );
93                                 correctedURL = url;
94                         }
95                 },
96
97                 open: function( editorId ) {
98                         var ed,
99                                 $body = $( document.body );
100
101                         $body.addClass( 'modal-open' );
102
103                         wpLink.range = null;
104
105                         if ( editorId ) {
106                                 window.wpActiveEditor = editorId;
107                         }
108
109                         if ( ! window.wpActiveEditor ) {
110                                 return;
111                         }
112
113                         this.textarea = $( '#' + window.wpActiveEditor ).get( 0 );
114
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 );
119
120                                 ed = tinymce.get( wpActiveEditor );
121
122                                 if ( ed && ! ed.isHidden() ) {
123                                         editor = ed;
124                                 } else {
125                                         editor = null;
126                                 }
127
128                                 if ( editor && tinymce.isIE ) {
129                                         editor.windowManager.bookmark = editor.selection.getBookmark();
130                                 }
131                         }
132
133                         if ( ! wpLink.isMCE() && document.selection ) {
134                                 this.textarea.focus();
135                                 this.range = document.selection.createRange();
136                         }
137
138                         inputs.wrap.show();
139                         inputs.backdrop.show();
140
141                         wpLink.refresh();
142
143                         $( document ).trigger( 'wplink-open', inputs.wrap );
144                 },
145
146                 isMCE: function() {
147                         return editor && ! editor.isHidden();
148                 },
149
150                 refresh: function() {
151                         var text = '';
152
153                         // Refresh rivers (clear links, check visibility)
154                         rivers.search.refresh();
155                         rivers.recent.refresh();
156
157                         if ( wpLink.isMCE() ) {
158                                 wpLink.mceRefresh();
159                         } else {
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' );
163                                 }
164
165                                 if ( document.selection ) {
166                                         // Old IE
167                                         text = document.selection.createRange().text || '';
168                                 } else if ( typeof this.textarea.selectionStart !== 'undefined' &&
169                                         ( this.textarea.selectionStart !== this.textarea.selectionEnd ) ) {
170                                         // W3C
171                                         text = this.textarea.value.substring( this.textarea.selectionStart, this.textarea.selectionEnd ) || '';
172                                 }
173
174                                 inputs.text.val( text );
175                                 wpLink.setDefaultValues();
176                         }
177
178                         if ( isTouch ) {
179                                 // Close the onscreen keyboard
180                                 inputs.url.focus().blur();
181                         } else {
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();
186                         }
187
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();
191                         }
192
193                         correctedURL = inputs.url.val().replace( /^http:\/\//, '' );
194                 },
195
196                 hasSelectedText: function( linkNode ) {
197                         var html = editor.selection.getContent();
198
199                         // Partial html and not a fully selected anchor element
200                         if ( /</.test( html ) && ( ! /^<a [^>]+>[^<]+<\/a>$/.test( html ) || html.indexOf('href=') === -1 ) ) {
201                                 return false;
202                         }
203
204                         if ( linkNode ) {
205                                 var nodes = linkNode.childNodes, i;
206
207                                 if ( nodes.length === 0 ) {
208                                         return false;
209                                 }
210
211                                 for ( i = nodes.length - 1; i >= 0; i-- ) {
212                                         if ( nodes[i].nodeType != 3 ) {
213                                                 return false;
214                                         }
215                                 }
216                         }
217
218                         return true;
219                 },
220
221                 mceRefresh: function() {
222                         var text,
223                                 selectedNode = editor.selection.getNode(),
224                                 linkNode = editor.dom.getParent( selectedNode, 'a[href]' ),
225                                 onlyText = this.hasSelectedText( linkNode );
226
227                         if ( 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 );
232                         } else {
233                                 text = editor.selection.getContent({ format: 'text' });
234                                 this.setDefaultValues();
235                         }
236
237                         if ( onlyText ) {
238                                 inputs.text.val( text || '' );
239                                 inputs.wrap.addClass( 'has-text-field' );
240                         } else {
241                                 inputs.text.val( '' );
242                                 inputs.wrap.removeClass( 'has-text-field' );
243                         }
244                 },
245
246                 close: function() {
247                         $( document.body ).removeClass( 'modal-open' );
248
249                         if ( ! wpLink.isMCE() ) {
250                                 wpLink.textarea.focus();
251
252                                 if ( wpLink.range ) {
253                                         wpLink.range.moveToBookmark( wpLink.range.getBookmark() );
254                                         wpLink.range.select();
255                                 }
256                         } else {
257                                 editor.focus();
258                         }
259
260                         inputs.backdrop.hide();
261                         inputs.wrap.hide();
262
263                         correctedURL = false;
264
265                         $( document ).trigger( 'wplink-close', inputs.wrap );
266                 },
267
268                 getAttrs: function() {
269                         wpLink.correctURL();
270
271                         return {
272                                 href: $.trim( inputs.url.val() ),
273                                 target: inputs.openInNewTab.prop( 'checked' ) ? '_blank' : ''
274                         };
275                 },
276
277                 buildHtml: function(attrs) {
278                         var html = '<a href="' + attrs.href + '"';
279
280                         if ( attrs.target ) {
281                                 html += ' target="' + attrs.target + '"';
282                         }
283
284                         return html + '>';
285                 },
286
287                 update: function() {
288                         if ( wpLink.isMCE() ) {
289                                 wpLink.mceUpdate();
290                         } else {
291                                 wpLink.htmlUpdate();
292                         }
293                 },
294
295                 htmlUpdate: function() {
296                         var attrs, text, html, begin, end, cursor, selection,
297                                 textarea = wpLink.textarea;
298
299                         if ( ! textarea ) {
300                                 return;
301                         }
302
303                         attrs = wpLink.getAttrs();
304                         text = inputs.text.val();
305
306                         // If there's no href, return.
307                         if ( ! attrs.href ) {
308                                 return;
309                         }
310
311                         html = wpLink.buildHtml(attrs);
312
313                         // Insert HTML
314                         if ( document.selection && wpLink.range ) {
315                                 // IE
316                                 // Note: If no text is selected, IE will not place the cursor
317                                 //       inside the closing tag.
318                                 textarea.focus();
319                                 wpLink.range.text = html + ( text || wpLink.range.text ) + '</a>';
320                                 wpLink.range.moveToBookmark( wpLink.range.getBookmark() );
321                                 wpLink.range.select();
322
323                                 wpLink.range = null;
324                         } else if ( typeof textarea.selectionStart !== 'undefined' ) {
325                                 // W3C
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;
331
332                                 // If no text is selected, place the cursor inside the closing tag.
333                                 if ( begin === end && ! selection ) {
334                                         cursor -= 4;
335                                 }
336
337                                 textarea.value = (
338                                         textarea.value.substring( 0, begin ) +
339                                         html +
340                                         textarea.value.substring( end, textarea.value.length )
341                                 );
342
343                                 // Update cursor position
344                                 textarea.selectionStart = textarea.selectionEnd = cursor;
345                         }
346
347                         wpLink.close();
348                         textarea.focus();
349                 },
350
351                 mceUpdate: function() {
352                         var attrs = wpLink.getAttrs(),
353                                 link, text;
354
355                         wpLink.close();
356                         editor.focus();
357
358                         if ( tinymce.isIE ) {
359                                 editor.selection.moveToBookmark( editor.windowManager.bookmark );
360                         }
361
362                         if ( ! attrs.href ) {
363                                 editor.execCommand( 'unlink' );
364                                 return;
365                         }
366
367                         link = getLink();
368
369                         if ( inputs.wrap.hasClass( 'has-text-field' ) ) {
370                                 text = inputs.text.val() || attrs.href;
371                         }
372
373                         if ( link ) {
374                                 if ( text ) {
375                                         if ( 'innerText' in link ) {
376                                                 link.innerText = text;
377                                         } else {
378                                                 link.textContent = text;
379                                         }
380                                 }
381
382                                 editor.dom.setAttribs( link, attrs );
383                         } else {
384                                 if ( text ) {
385                                         editor.selection.setNode( editor.dom.create( 'a', attrs, text ) );
386                                 } else {
387                                         editor.execCommand( 'mceInsertLink', false, attrs );
388                                 }
389                         }
390
391                         editor.nodeChanged();
392                 },
393
394                 updateFields: function( e, li ) {
395                         inputs.url.val( li.children( '.item-permalink' ).val() );
396                 },
397
398                 setDefaultValues: function() {
399                         var selection,
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;
402
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 );
409                         }
410
411                         if ( selection && emailRegexp.test( selection ) ) {
412                                 // Selection is email address
413                                 inputs.url.val( 'mailto:' + selection );
414                         } else if ( selection && urlRegexp.test( selection ) ) {
415                                 // Selection is URL
416                                 inputs.url.val( selection.replace( /&amp;|&#0?38;/gi, '&' ) );
417                         } else {
418                                 // Set URL to default.
419                                 inputs.url.val( '' );
420                         }
421
422                         // Update save prompt.
423                         inputs.submit.val( wpLinkL10n.save );
424                 },
425
426                 searchInternalLinks: function() {
427                         var t = $( this ), waiting,
428                                 search = t.val();
429
430                         if ( search.length > 2 ) {
431                                 rivers.recent.hide();
432                                 rivers.search.show();
433
434                                 // Don't search if the keypress didn't change the title.
435                                 if ( wpLink.lastSearch == search )
436                                         return;
437
438                                 wpLink.lastSearch = search;
439                                 waiting = t.parent().find( '.spinner' ).addClass( 'is-active' );
440
441                                 rivers.search.change( search );
442                                 rivers.search.ajax( function() {
443                                         waiting.removeClass( 'is-active' );
444                                 });
445                         } else {
446                                 rivers.search.hide();
447                                 rivers.recent.show();
448                         }
449                 },
450
451                 next: function() {
452                         rivers.search.next();
453                         rivers.recent.next();
454                 },
455
456                 prev: function() {
457                         rivers.search.prev();
458                         rivers.recent.prev();
459                 },
460
461                 keydown: function( event ) {
462                         var fn, id,
463                                 key = $.ui.keyCode;
464
465                         if ( key.ESCAPE === event.keyCode ) {
466                                 wpLink.close();
467                                 event.stopImmediatePropagation();
468                         } else if ( key.TAB === event.keyCode ) {
469                                 id = event.target.id;
470
471                                 // wp-link-submit must always be the last focusable element in the dialog.
472                                 // following focusable elements will be skipped on keyboard navigation.
473                                 if ( id === 'wp-link-submit' && ! event.shiftKey ) {
474                                         inputs.close.focus();
475                                         event.preventDefault();
476                                 } else if ( id === 'wp-link-close' && event.shiftKey ) {
477                                         inputs.submit.focus();
478                                         event.preventDefault();
479                                 }
480                         }
481
482                         if ( event.keyCode !== key.UP && event.keyCode !== key.DOWN ) {
483                                 return;
484                         }
485
486                         if ( document.activeElement &&
487                                 ( document.activeElement.id === 'link-title-field' || document.activeElement.id === 'url-field' ) ) {
488                                 return;
489                         }
490
491                         fn = event.keyCode === key.UP ? 'prev' : 'next';
492                         clearInterval( wpLink.keyInterval );
493                         wpLink[ fn ]();
494                         wpLink.keyInterval = setInterval( wpLink[ fn ], wpLink.keySensitivity );
495                         event.preventDefault();
496                 },
497
498                 keyup: function( event ) {
499                         var key = $.ui.keyCode;
500
501                         if ( event.which === key.UP || event.which === key.DOWN ) {
502                                 clearInterval( wpLink.keyInterval );
503                                 event.preventDefault();
504                         }
505                 },
506
507                 delayedCallback: function( func, delay ) {
508                         var timeoutTriggered, funcTriggered, funcArgs, funcContext;
509
510                         if ( ! delay )
511                                 return func;
512
513                         setTimeout( function() {
514                                 if ( funcTriggered )
515                                         return func.apply( funcContext, funcArgs );
516                                 // Otherwise, wait.
517                                 timeoutTriggered = true;
518                         }, delay );
519
520                         return function() {
521                                 if ( timeoutTriggered )
522                                         return func.apply( this, arguments );
523                                 // Otherwise, wait.
524                                 funcArgs = arguments;
525                                 funcContext = this;
526                                 funcTriggered = true;
527                         };
528                 },
529
530                 toggleInternalLinking: function( event ) {
531                         var visible = inputs.wrap.hasClass( 'search-panel-visible' );
532
533                         inputs.wrap.toggleClass( 'search-panel-visible', ! visible );
534                         setUserSetting( 'wplink', visible ? '0' : '1' );
535                         inputs[ ! visible ? 'search' : 'url' ].focus();
536                         event.preventDefault();
537                 }
538         };
539
540         River = function( element, search ) {
541                 var self = this;
542                 this.element = element;
543                 this.ul = element.children( 'ul' );
544                 this.contentHeight = element.children( '#link-selector-height' );
545                 this.waiting = element.find('.river-waiting');
546
547                 this.change( search );
548                 this.refresh();
549
550                 $( '#wp-link .query-results, #wp-link #link-selector' ).scroll( function() {
551                         self.maybeLoad();
552                 });
553                 element.on( 'click', 'li', function( event ) {
554                         self.select( $( this ), event );
555                 });
556         };
557
558         $.extend( River.prototype, {
559                 refresh: function() {
560                         this.deselect();
561                         this.visible = this.element.is( ':visible' );
562                 },
563                 show: function() {
564                         if ( ! this.visible ) {
565                                 this.deselect();
566                                 this.element.show();
567                                 this.visible = true;
568                         }
569                 },
570                 hide: function() {
571                         this.element.hide();
572                         this.visible = false;
573                 },
574                 // Selects a list item and triggers the river-select event.
575                 select: function( li, event ) {
576                         var liHeight, elHeight, liTop, elTop;
577
578                         if ( li.hasClass( 'unselectable' ) || li == this.selected )
579                                 return;
580
581                         this.deselect();
582                         this.selected = li.addClass( 'selected' );
583                         // Make sure the element is visible
584                         liHeight = li.outerHeight();
585                         elHeight = this.element.height();
586                         liTop = li.position().top;
587                         elTop = this.element.scrollTop();
588
589                         if ( liTop < 0 ) // Make first visible element
590                                 this.element.scrollTop( elTop + liTop );
591                         else if ( liTop + liHeight > elHeight ) // Make last visible element
592                                 this.element.scrollTop( elTop + liTop - elHeight + liHeight );
593
594                         // Trigger the river-select event
595                         this.element.trigger( 'river-select', [ li, event, this ] );
596                 },
597                 deselect: function() {
598                         if ( this.selected )
599                                 this.selected.removeClass( 'selected' );
600                         this.selected = false;
601                 },
602                 prev: function() {
603                         if ( ! this.visible )
604                                 return;
605
606                         var to;
607                         if ( this.selected ) {
608                                 to = this.selected.prev( 'li' );
609                                 if ( to.length )
610                                         this.select( to );
611                         }
612                 },
613                 next: function() {
614                         if ( ! this.visible )
615                                 return;
616
617                         var to = this.selected ? this.selected.next( 'li' ) : $( 'li:not(.unselectable):first', this.element );
618                         if ( to.length )
619                                 this.select( to );
620                 },
621                 ajax: function( callback ) {
622                         var self = this,
623                                 delay = this.query.page == 1 ? 0 : wpLink.minRiverAJAXDuration,
624                                 response = wpLink.delayedCallback( function( results, params ) {
625                                         self.process( results, params );
626                                         if ( callback )
627                                                 callback( results, params );
628                                 }, delay );
629
630                         this.query.ajax( response );
631                 },
632                 change: function( search ) {
633                         if ( this.query && this._search == search )
634                                 return;
635
636                         this._search = search;
637                         this.query = new Query( search );
638                         this.element.scrollTop( 0 );
639                 },
640                 process: function( results, params ) {
641                         var list = '', alt = true, classes = '',
642                                 firstPage = params.page == 1;
643
644                         if ( ! results ) {
645                                 if ( firstPage ) {
646                                         list += '<li class="unselectable no-matches-found"><span class="item-title"><em>' +
647                                                 wpLinkL10n.noMatchesFound + '</em></span></li>';
648                                 }
649                         } else {
650                                 $.each( results, function() {
651                                         classes = alt ? 'alternate' : '';
652                                         classes += this.title ? '' : ' no-title';
653                                         list += classes ? '<li class="' + classes + '">' : '<li>';
654                                         list += '<input type="hidden" class="item-permalink" value="' + this.permalink + '" />';
655                                         list += '<span class="item-title">';
656                                         list += this.title ? this.title : wpLinkL10n.noTitle;
657                                         list += '</span><span class="item-info">' + this.info + '</span></li>';
658                                         alt = ! alt;
659                                 });
660                         }
661
662                         this.ul[ firstPage ? 'html' : 'append' ]( list );
663                 },
664                 maybeLoad: function() {
665                         var self = this,
666                                 el = this.element,
667                                 bottom = el.scrollTop() + el.height();
668
669                         if ( ! this.query.ready() || bottom < this.contentHeight.height() - wpLink.riverBottomThreshold )
670                                 return;
671
672                         setTimeout(function() {
673                                 var newTop = el.scrollTop(),
674                                         newBottom = newTop + el.height();
675
676                                 if ( ! self.query.ready() || newBottom < self.contentHeight.height() - wpLink.riverBottomThreshold )
677                                         return;
678
679                                 self.waiting.addClass( 'is-active' );
680                                 el.scrollTop( newTop + self.waiting.outerHeight() );
681
682                                 self.ajax( function() {
683                                         self.waiting.removeClass( 'is-active' );
684                                 });
685                         }, wpLink.timeToTriggerRiver );
686                 }
687         });
688
689         Query = function( search ) {
690                 this.page = 1;
691                 this.allLoaded = false;
692                 this.querying = false;
693                 this.search = search;
694         };
695
696         $.extend( Query.prototype, {
697                 ready: function() {
698                         return ! ( this.querying || this.allLoaded );
699                 },
700                 ajax: function( callback ) {
701                         var self = this,
702                                 query = {
703                                         action : 'wp-link-ajax',
704                                         page : this.page,
705                                         '_ajax_linking_nonce' : inputs.nonce.val()
706                                 };
707
708                         if ( this.search )
709                                 query.search = this.search;
710
711                         this.querying = true;
712
713                         $.post( ajaxurl, query, function( r ) {
714                                 self.page++;
715                                 self.querying = false;
716                                 self.allLoaded = ! r;
717                                 callback( r, query );
718                         }, 'json' );
719                 }
720         });
721
722         $( document ).ready( wpLink.init );
723 })( jQuery );