Changeset 2083624
- Timestamp:
- 05/08/2019 01:42:30 PM (7 years ago)
- Location:
- traveladsnetwork-com
- Files:
-
- 1 added
- 6 edited
-
svn_manual.md (added)
-
tags/1.0/assets/css/pageEdit.css (modified) (2 diffs)
-
tags/1.0/assets/js/pageEdit.js (modified) (1 diff)
-
tags/1.0/readme.txt (modified) (1 diff)
-
trunk/assets/css/pageEdit.css (modified) (2 diffs)
-
trunk/assets/js/pageEdit.js (modified) (1 diff)
-
trunk/readme.txt (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
traveladsnetwork-com/tags/1.0/assets/css/pageEdit.css
r2083453 r2083624 20 20 table#navMenu 21 21 { 22 box-sizing: border-box; 22 23 width:100%; 23 24 position:absolute; … … 28 29 padding:0 20px; 29 30 z-index: 1; 31 32 transition: border-radius .3s, 33 box-shadow .3s; 34 } 35 table#navMenu.flying { 36 border-radius: 0 0 10px 10px; 37 box-shadow: 0px 5px 10px 5px #0003; 30 38 } 31 39 table#navMenu td -
traveladsnetwork-com/tags/1.0/assets/js/pageEdit.js
r2083453 r2083624 15 15 16 16 !function($) { 17 $(document).ready(function() { 18 // {{{ 19 20 /** 21 * Changes the current focussed word 22 * 23 * @param {int} nb The keyWord to highlight -> e.g. if you want to highlight the 5th keyword, nb=4 24 * @param {bool} scroll Should the screen scroll automatically to this keyword? 25 */ 26 function tan_changeFocus(nb=0, scroll=true) 27 { 28 // {{{ 29 nb = nb || 0; 30 31 nb = (nb+$('#travel_editor mark.TANKeyWord').length)%$('#travel_editor mark.TANKeyWord').length 32 33 currentFocus = nb; 34 elem = $('#travel_editor mark.TANKeyWord')[nb]; 35 36 $('#navMenu_keyWord').val(elem.innerHTML.trim()); 37 38 $('#travel_editor mark.TANKeyWord').removeClass('current'); 39 $(elem).addClass('current'); 40 41 $('#navMenu_currentKeyWord').text(nb+1); 42 43 if($(elem).hasClass('active')) 44 { 45 $('div#navMenu_buttonOnOff').addClass('active'); 46 } 47 else 48 { 49 $('div#navMenu_buttonOnOff').removeClass('active'); 50 } 51 52 53 $('select#navMenu_merchantSelect').val($(elem).attr('data-adv') || $('select#navMenu_merchantSelect option')[0].value); 54 55 if(scroll) 56 { 57 $('html, body').clearQueue(); 58 $('html, body').animate({ 59 scrollTop: $(elem).offset().top-$(window).height()/2 60 }, 500); 61 } 62 // }}} 63 } 64 65 function setWpEditorContent(content) 66 { 67 // {{{ 68 if(!window.editorName) 69 window.editorName = (!!wp.data)?'gutenberg': 70 (!!tinymce || !!tinyMCE)?'tinymce': 71 ''; 72 73 switch(editorName) 74 { 75 case 'gutenberg': 76 wp.data.dispatch( 'core/editor' ).resetBlocks([]); 77 78 var editedContent = wp.data.select( "core/editor" ).getEditedPostContent(); 79 var newBlocks = wp.blocks.parse( content ); 80 wp.data.dispatch( "core/editor" ).insertBlocks( newBlocks ); 81 82 return true; 83 case 'tinymce': 84 85 if(!!tinymce.activeEditor) 86 tinymce.activeEditor.setContent( content ) 87 else if(!!tinyMCE.activeEditor) 88 tinyMCE.activeEditor.setContent( content ) 89 90 $('#content').val($('#hidden_travel_editor').html()); 91 92 return true; 93 } 94 95 return false; 96 // }}} 97 } 98 99 function getWpEditorContent() 100 { 101 // {{{ 102 if(!window.editorName) 103 window.editorName = (!!wp.data)?'gutenberg': 104 (!!tinymce || !!tinyMCE)?'tinymce': 105 ''; 106 107 switch(editorName) 108 { 109 case 'gutenberg': 110 return wp.data.select('core/editor').getEditedPostContent(); 111 case 'tinymce': 112 if($('#content').attr('aria-hidden') === 'true') // text editor 113 return (window.tinymce || window.tinyMCE).activeEditor.getContent(); 114 else 115 return $('#content').val().replace(/\n/g, '<br/>'); 116 } 117 118 return ''; 119 // }}} 120 } 121 122 123 /** 124 * Finds the position of a keyWord 125 * 126 * @param {DomNode} e The Dom node representing the keyword, has to be a `<mark class='TANKeyWord'>` 127 * @param {jQueryNode} container The container in which the keyWords are. travel editor by default. 128 * 129 * @return {number} the position of this specific keyword, or 0 if not found 130 */ 131 132 function tan_domToNum(e, container=$('#travel_editor')) 133 { 134 // {{{ 135 list = container.find(' mark.TANKeyWord'); 136 for(var i=0 ; i<list.length ; i++) 137 if(list[i] === e) 138 return i; 139 return 0; 140 // }}} 141 } 142 143 /** 144 * function called when a keyWord is clicked 145 * 146 * @param {DomNode} e The clicked keyWord, has to be a `<mark class='TANKeyWord'>` 147 */ 148 function tan_clickMark(e) 149 { 150 tan_changeFocus(tan_domToNum(e)); 151 } 152 153 /** 154 * Toggle the specified keyWord on and off 155 * 156 * @param {number} foc The keyword to activate, the currently focussed keyword by default, pass -1 to toggle all, -2 to toggle all custom keywords 157 * @param {bool} flag Weather the keyWord should be activated or deactivated. Toggle by default 158 */ 159 function tan_activateKeyWord(foc = currentFocus, flag = undefined) 160 { 161 // {{{ 162 if(foc < 0) 163 { 164 let classes = ['TANKeyWord']; 165 if(foc === -2) 166 classes.push('custom'); 167 168 let classesStr = classes.map(v=>'.'+v).join(''); 169 170 f = $('#travel_editor mark.active'+classesStr).length 171 - $('#travel_editor mark'+classesStr+':not(.active)').length 172 173 174 f = f<0; 175 176 $('#travel_editor mark'+classesStr).each(function() { 177 tan_activateKeyWord(tan_domToNum(this), f); 178 }); 179 180 return; 181 } 182 183 elem = $('#travel_editor mark.TANKeyWord').eq(foc); 184 185 if(elem.hasClass('active') === flag) 186 return; 187 188 if(flag === undefined) 189 elem.toggleClass('active'); 190 else 191 elem.toggleClass('active', flag); 192 193 elem.attr('data-adv', $('#navMenu_merchantSelect').find('option:selected').val()); 194 195 196 if(foc === currentFocus) $('#navMenu_buttonOnOff').toggleClass('active', elem.hasClass('active')); 197 198 // }}} 199 } 200 201 var totalReplaced = 0; 202 203 /** 204 * Surround the keyWords by <mark>, to highlighting it 205 * 206 * @param {RegExp} target regular expression matching the targeted keywords 207 */ 208 jQuery.fn.tan_highlightKeywords = function(target) 209 { 210 // {{{ 211 // 212 // Get all text nodes: 213 // 214 excludeNodes = ['SCRIPT', 'STYLE', 'H1', 'H2', 'H3', 'H4']; 215 var $textNodes = this 216 .find("*") 217 .andSelf() 218 .contents() 219 .filter(function() { 220 return this.nodeType === 3 221 && !$(this).parent("a").length 222 && !$(this).parent(excludeNodes.join(',')).length 223 && !$(this).parent('.TANKeyWord').length 224 && !$(this).parent('.TAN_link').length 225 && $(this).text().trim() !== ''; 226 }); 227 228 // 229 // Iterate through the text nodes, 230 // highlighting the keyWords 231 // 232 233 let totalReplaced = 0; 234 $textNodes.each(function(index, element) { 235 236 match = $(element).text().match(target); 237 238 if(match === null) return; 239 var i=0; 240 var finalStr = $(element).text().replace(target, function(word) { 241 var originalWord = match[i]; 242 i++; 243 totalReplaced++; 244 245 let tmp = addedKeyWords.filter(v=>v[0].format() === originalWord.format()) 246 247 let c = ''; 248 let s = originalWord; 249 if(tmp.length) 250 { 251 c = ' custom' 252 s = tmp[0][1]; 253 } 254 255 return `<mark class='TANKeyWord`+c+`' 256 data-search='`+s+`' 257 data-adv=`+$('#navMenu_merchantSelect').find('option:selected').val()+`>` 258 + originalWord 259 + `</mark>`; 260 }); 261 262 $(element).replaceWith(finalStr); 263 }); 264 265 $('.TANKeyWord').each(function(i) { 266 267 this.onclick = function() { 268 tan_changeFocus(tan_domToNum(this)); 269 }; 270 }); 271 272 // }}} 273 } 274 275 /** 276 * copy content from the travel ads network editor to the tiny MCE 277 */ 278 function copy_travel2wp() 279 { 280 // {{{ 281 282 $('#hidden_travel_editor').html(($('div#travel_editor').html())); 283 284 $('#hidden_travel_editor mark.TANKeyWord').each(function() { 285 $(this).replaceWith(this.outerHTML.replace(/\bmark\b/, 'span')) 286 }); 287 288 $('#hidden_travel_editor span.TANKeyWord').each(function() { 289 $(this).removeClass('current'); 290 $(this).removeClass('TANKeyWord'); 291 292 $(this).addClass('TAN_link'); 293 294 if($(this).hasClass('custom')) 295 $(this).addClass('TAN_link_custom'); 296 }); 297 298 $('#hidden_travel_editor section.tan_asset').each(function(i) { 299 $(this).find('script').siblings().remove(); 300 }); 301 302 setWpEditorContent( $('#hidden_travel_editor').html() ); 303 304 $('div#travel_editor section.tan_asset script').remove(); 305 306 $('#hidden_travel_editor').empty(); 307 308 // }}} 309 } 310 311 /** 312 * Copy the content from the wordpress editor to the travel editor 313 * 314 * @param {string} content The content to put in the travel editor 315 */ 316 function copy_wp2travel() 317 { 318 // {{{ 319 content = getWpEditorContent(); 320 321 322 if( $('div.mce-tinymce').css('display') === 'none' // normal mode 323 && $('wp-editor-area').css('display') === 'none') // text mode 324 { 325 $('#travel_editor').html('<h1 style="text-align:center">The visual Editor is not supported for now...</h1>'+ 326 '<h2 style="text-align:center">You can switch to autmatic mode!</h2>'); 327 $('#navMenu').toggle(false); 328 $('#navMenuOffset').toggle(false); 329 return; 330 } 331 $('#navMenu').toggle(true); 332 $('#navMenuOffset').toggle(true); 333 334 totalReplaced = 0; 335 $('#travel_editor').empty(); 336 $('#travel_editor').html(content); 337 $('#travel_editor span.TAN_link').each(function() { 338 339 $(this).replaceWith(this.outerHTML.replace(/\bspan\b/, 'mark')) 340 }); 341 342 $('#travel_editor mark.TAN_link').each(function() { 343 $(this).removeClass('TAN_link_custom'); 344 $(this).removeClass('TAN_link'); 345 346 $(this).addClass('TANKeyWord'); 347 }); 348 349 350 $("#travel_editor").tan_highlightKeywords(createNewRegexp(keyWordsRegExp, addedKeyWords.map(v=>v[0]))); 351 352 console.log('here'); 353 tan_changeFocus(currentFocus, false); 354 console.log('not here'); 355 $('#navMenu_totalKeyWord').text($('#travel_editor mark.TANKeyWord').length); 356 // }}} 357 } 358 359 /** 360 * Transforms the google places API response to a easier to use object 361 * 362 * @param {Object} googlePlace The object returned by the google places API 363 * -> ``` 364 * { 365 * address_coponents: <array of addresses>, 366 * geometry: <geometry, lat, long>, 367 * types: <array of types>, 368 * name: <name> 369 * } 370 * ``` 371 * 372 * @return {Object} An easier to use array 373 * -> ``` 374 * { 375 * country: <country name>, 376 * city: <city name>, 377 * route: <route name>, 378 * searchText: <text to search on the OTA's calls>, 379 * lat: <latitude>, 380 * lng: <longitude>, 381 * } 382 * 383 * ``` 384 */ 385 function googlePlaceFormat(googlePlace) 386 { 387 // {{{ 388 let out = {}; 389 390 let cityWrongNames = { 391 'Krung Thep Maha Nakhon': 'Bangkok', 392 'Krong Siem Reap': 'Siem Reap' 393 }; 394 395 let address = googlePlace.address_components.filter(v=> !/^[0-9]*$/.test(v.long_name) // exclude only numbers 396 && !/^[0-9]/.test(v.long_name)); // and beginning with number 397 398 let country = address.filter(v=>v.types.contains('country')), 399 administrativeAreas = address.filter(v=>v.types.some(t=>/administrative_area_level_\d+/.test(t))), 400 city = address.filter(v=>v.types.contains('locality', 'postal_town')), 401 subLocalAreas = address.filter(v=>v.types.some(t=>/sublocality_level_\d+/.test(t))), 402 route = address.filter(v=>v.types.contains('route')); 403 404 out = { 405 'lat': googlePlace.geometry.location.lat(), 406 'lng': googlePlace.geometry.location.lng() 407 }; 408 409 if(country.length) 410 { 411 out.country=country[0].long_name; 412 if(city.length) 413 { 414 out.city=city[0].long_name; 415 416 if(route.length) 417 out.route = route[0].long_name 418 else if(subLocalAreas.length) 419 out.route = subLocalAreas[0].long_name 420 } 421 else if(administrativeAreas.length) 422 out.city = administrativeAreas[0].long_name; 423 } 424 425 out.city = cityWrongNames[out.city] || out.city; 426 427 428 if(!googlePlace.types.contains('lodging', 'premise')) // is not lodging nor premise 429 out.searchText = (out.route||'')+', '+(out.city||'')+', '+(out.country||''); 430 else 431 out.searchText = (googlePlace.name||'')+', '+(out.city||'')+', '+(out.country||''); 432 433 out.searchText = out.searchText.replace(/^[, ]+/, ''); 434 435 return out; 436 // }}} 437 } 438 439 /** 440 * Adds the custom keyWords to the original regexp 441 * 442 * @param {RegExp} original The original regexp conaining all the default keywords 443 * @param {Array} keyWords A list of the new keyWords 444 */ 445 function createNewRegexp(original, keyWords) 446 { 447 // {{{ 448 // 449 // transforming the old regexp to string 450 // 451 let newRegExp = original.toString().replace(/^[^\/]*\/|\/[^/]*$/g, ''); 452 453 // 454 // adding the new keyWords 455 // 456 for(let i=0 ; i<keyWords.length ; i++) 457 { 458 newRegExp += '|'; 459 if(/^[a-z]/i.test(keyWords[i])) // begin with latin alphabet 460 newRegExp += '\\b'; 461 462 newRegExp += keyWords[i].replace(/([\[\]\(\)\+\/\*\?])/g, '\\$1'); // making the word regex proof 463 464 if(/[a-z]$/i.test(keyWords[i])) // finish with latin alphabet 465 newRegExp += '\\b'; 466 467 newRegExp += '|'; 468 if(/^[a-z]/i.test(keyWords[i])) // begin with latin alphabet 469 newRegExp += '\\b'; 470 471 newRegExp += keyWords[i].format().replace(/([\[\]\(\)\+\/\*\?])/g, '\\$1'); // making the word regex proof 472 473 if(/[a-z]$/i.test(keyWords[i])) // finish with latin alphabet 474 newRegExp += '\\b'; 475 476 } 477 478 return new RegExp(newRegExp, 'gi'); 479 // }}} 480 } 481 482 /** 483 * initialize the google places field 484 */ 485 function initAutocomplete() 486 { 487 // {{{ 488 // 489 // avoids to trigger the page reload on enter 490 // 491 $('#googleField').on('keydown', function(e){if(e.keyCode===13) e.preventDefault()}); 492 493 googleField = new google.maps.places.Autocomplete($('#googleField')[0], 494 {types: ['establishment', 'geocode']}); 495 496 googleField.setFields(['address_components', 497 'geometry', 498 'types', 499 'name']); 500 501 googleField.addListener('place_changed', fillInAddress); 502 // }}} 503 } 504 505 /** 506 * Callback, listen to google places field 507 */ 508 function fillInAddress() 509 { 510 // {{{ 511 var place = googleField.getPlace(); 512 513 $('#googleField').addClass('kwAdd'); 514 let kw = $('#googleField').val().split(/, ?/)[0] 515 516 if(lastSel[0]) 517 { 518 let start = $(lastSel[1].startContainer).parents('mark.TANKeyWord'); 519 let end = $(lastSel[1].endContainer).parents('mark.TANKeyWord'); 520 521 lastSel[1].deleteContents(); 522 lastSel[1].insertNode(document.createTextNode(' '+kw+' ')); 523 524 start.replaceWith(function() { return this.innerHTML }); 525 end.replaceWith(function() { return this.innerHTML }); 526 } 527 528 if( !addedKeyWords.map(v=>v[0].format()).contains(kw.format()) 529 && ( !kw.match(keyWordsRegExp) 530 || kw.match(keyWordsRegExp)[0] !== kw )) // kw not present in predefined keywords 531 addedKeyWords.push([kw, googlePlaceFormat(place).searchText]); 532 533 if(addedKeyWords.length) 534 $('#newKWs').html('<br/>Your keywords:<br/> '+addedKeyWords.map(v=>'<span class=newKW>'+v[0]+'</span>').join(' ')); 535 536 537 $('span.newKW').click(function() { 538 let kw = this.innerHTML; 539 540 addedKeyWords = addedKeyWords.filter(v=>v[0] !== kw); 541 $('.TANKeyWord').each(function() { 542 if(kw.format() === this.innerText.format()) 543 $(this).replaceWith(this.innerHTML); 544 }); 545 $(this).remove(); 546 }); 547 548 549 $("#travel_editor").tan_highlightKeywords(createNewRegexp(keyWordsRegExp, addedKeyWords.map(v=>v[0]))); 550 // }}} 551 } 552 553 // 554 // setting up the listeners 555 // 556 557 $('div#navMenu_buttonOnOff').click(function(){ 558 tan_activateKeyWord(); 559 }); 560 561 $('div[aria-label="TravelAdsNetwork Manual"]').click(function() { 562 // {{{ 563 if($('#travelads_sectionid').css('display') === 'none') 564 { 565 setTimeout(function() { 566 $('html, body').animate({ 567 scrollTop: $('#travelads_sectionid').offset().top - 100 568 }, 500); 569 }, 300); 570 } 571 // }}} 572 }); 573 574 $('#navMenu_merchantSelect').change(function() { 575 $('.TANKeyWord.current').attr('data-adv', $(this).find('option:selected').val()) 576 tan_activateKeyWord(undefined, true); 577 }); 578 579 $('#navMenuprevWord').click(function() { 580 tan_changeFocus(currentFocus-1); 581 }); 582 $('#navMenunextWord').click(function() { 583 tan_changeFocus(currentFocus+1); 584 }); 585 586 $('#publish').click(function() { 587 copy_travel2wp(); 588 }); 589 590 591 let stop=setInterval(function() { 592 // {{{ 593 if(!(window.tinyMCE || window.tinymce).editors) 594 return; 595 596 clearInterval(stop); 597 598 $('#travelads_sectionid').on('mouseenter', function() { 599 if(!travelFocused) 600 { 601 travelFocused = true; 602 copy_wp2travel(); 603 } 604 605 let editors = ['#content', '.editor-writing-flow'] 606 .concat([...(window.tinyMCE || window.tinymce).editors] 607 .map(v=>(!!v.container && '#'+v.container.getAttribute('id') || ''))) 608 .filter(v=>!!v) 609 .join(', '); 610 611 $(editors).each(function() { 612 this.onmouseenter = function() { 613 if(travelFocused) 614 copy_travel2wp(); 615 travelFocused = false; 616 }; 617 }); 618 }); 619 620 let content = getWpEditorContent(); 621 622 var parser = new DOMParser(); 623 data = parser.parseFromString('<html>'+content.replace(/ /g, '<br/>')+'</html>', "text/xml"); 624 625 [...data.getElementsByClassName('TAN_link_custom')].forEach(function(elem) { 626 let flag=-2; 627 addedKeyWords.forEach(function([kw], index) { 628 if(kw.format() === elem.innerHTML.format()) 629 if(kw.format() !== kw) 630 flag = index; 631 else 632 flag = -1; 633 }); 634 635 switch(flag) 636 { 637 case -2: 638 addedKeyWords.push([elem.innerHTML, elem.getAttribute('data-search')]); 639 break; 640 case -1: 641 break; 642 default: 643 addedKeyWords[flag] = [elem.innerHTML, elem.getAttribute('data-search')]; 644 } 645 }); 646 647 if(addedKeyWords.length) 648 $('#newKWs').html('<br/>Your keywords:<br/> '+addedKeyWords.map(v=>'<span class=newKW>'+v[0]+'</span>').join(', ')); 649 // }}} 650 }, 500); 651 652 653 let ctrlDown = false; 654 let shiftDown = false; 655 let lastSel = []; 656 657 /** 658 * Fast mode, allow to go through the keywords 659 * with the keyboard, much faster than the mouse 660 */ 661 $('#navMenu_keyWord').on('keydown', function(e) { 662 // {{{ 663 let code = e.keyCode; 664 let sel = $("#navMenu_merchantSelect"); 665 switch(code) 666 { 667 case 39: // right 668 tan_changeFocus(currentFocus+1); 669 break; 670 case 37: 671 tan_changeFocus(currentFocus-1); 672 break; // left 673 case 38: // up 674 sel.val((sel.find(':selected').prev('option')[0] || sel.find('option:last-child')[0]).value); 675 sel.trigger('change'); 676 break; 677 case 40: // down 678 sel.val((sel.find(':selected + option')[0] || sel.find('option:first-child')[0]).value); 679 sel.trigger('change'); 680 break; 681 case 32: tan_activateKeyWord(ctrlDown?-1:shiftDown?-2:currentFocus); break; // space 682 case 13: tan_activateKeyWord(ctrlDown?-1:shiftDown?-2:currentFocus); break; // enter 683 684 case 17: ctrlDown=true; break; 685 case 16: shiftDown=true; break; 686 } 687 688 e.preventDefault(); 689 // }}} 690 }) 691 $('#navMenu_keyWord').on('keyup', function(e) { 692 // {{{ 693 if(e.keyCode === 17) 694 ctrlDown=false; 695 if(e.keyCode === 16) 696 shiftDown=false; 697 // }}} 698 }); 699 700 $('#travel_editor, #navMenu tr:last-child').on('mouseup', function(e) { 701 // {{{ 702 setTimeout(function() { 703 if( !$(e.target).is("#navMenu_merchantSelect") 704 && !getSelectionHtml()[0] 705 && !$('#googleField').is(':focus')) 706 $('#navMenu_keyWord')[0].focus(); 707 }, 100); 708 // }}} 709 }) 710 711 $('#travel_editor').on('mouseup', function(e) { 712 // {{{ 713 let sel = getSelectionHtml(); 714 if(!sel[0]) 715 return; 716 717 lastSel = sel; 718 $(lastSel[1]).parents('mark.TANKeyWord').replaceWith(function() { 719 console.log('this', this); 720 return this.innerHTML; 721 }) 722 sel = sel[0]; 723 724 $('#googleField').val(sel.replace(/<[^>]*>/g, '')); 725 $('#googleField')[0].focus(); 726 // }}} 727 }); 728 729 document.addEventListener('scroll', function (e) { 730 // {{{ 731 let diff = $(window).scrollTop() - $('#travelads_sectionid .inside').offset().top + 32; 732 733 if(diff >= 0) 734 { 735 $('#navMenu').css({ 736 position: 'fixed', 737 left: $('#travelads_sectionid').offset().left, 738 top: $('#wpadminbar').height(), 739 740 width: $('#travelads_sectionid').width()+2, 741 }); 742 743 $('.pac-container.pac-logo').css({ 744 top: $('#googleField').offset().top + $('#googleField').height() + 8, 745 }); 746 747 } 748 else 749 { 750 $('#navMenu').css({ 751 position: '', 752 left: '', 753 top: '', 754 width: '' 755 }); 756 } 757 // }}} 758 }, true); 759 760 initAutocomplete(); 761 762 763 // 764 // creation of the Regex (list of keywords) and initializations 765 // 766 data=keyWords; 767 var to_replace = ''; 768 for(var i=0 ; i<data.length ; i++) 769 to_replace += '\\b'+data[i]+'\\b|'; 770 771 to_replace = to_replace.slice(0, -1); 772 773 keyWordsRegExp = new RegExp(to_replace, 'gi') 774 775 776 // 777 // Open the dialog by default 778 // 779 setTimeout(function() { 780 $('#travelads_sectionid.closed button').click(); 781 }, 2000); 782 783 // }}} 784 }); 17 $(document).ready(function() { 18 // {{{ 19 20 /** 21 * Changes the current focussed word 22 * 23 * @param {int} nb The keyWord to highlight -> e.g. if you want to highlight the 5th keyword, nb=4 24 * @param {bool} scroll Should the screen scroll automatically to this keyword? 25 */ 26 function tan_changeFocus(nb=0, scroll=true) 27 { 28 // {{{ 29 nb = nb || 0; 30 31 nb = (nb+$('#travel_editor mark.TANKeyWord').length)%$('#travel_editor mark.TANKeyWord').length 32 33 currentFocus = nb; 34 elem = $('#travel_editor mark.TANKeyWord')[nb]; 35 36 $('#navMenu_keyWord').val(elem.innerHTML.trim()); 37 38 $('#travel_editor mark.TANKeyWord').removeClass('current'); 39 $(elem).addClass('current'); 40 41 $('#navMenu_currentKeyWord').text(nb+1); 42 43 if($(elem).hasClass('active')) 44 { 45 $('div#navMenu_buttonOnOff').addClass('active'); 46 } 47 else 48 { 49 $('div#navMenu_buttonOnOff').removeClass('active'); 50 } 51 52 53 $('select#navMenu_merchantSelect').val($(elem).attr('data-adv') || $('select#navMenu_merchantSelect option')[0].value); 54 55 if(scroll) 56 { 57 switch( window.editorName ) { 58 case 'gutenberg': 59 $('.edit-post-layout__content').clearQueue(); 60 $('.edit-post-layout__content').animate({ 61 scrollTop: $(elem).offset().top-$('.edit-post-visual-editor').offset().top-$(window).height()/2 62 }, 500); 63 break; 64 case 'tinymce': 65 $('html, body').clearQueue(); 66 $('html, body').animate({ 67 scrollTop: $(elem).offset().top-$(window).height()/2 68 }, 500); 69 break; 70 } 71 } 72 // }}} 73 } 74 75 function setWpEditorContent(content) 76 { 77 // {{{ 78 if(!window.editorName) 79 window.editorName = (!!wp.data)?'gutenberg': 80 (!!tinymce || !!tinyMCE)?'tinymce': 81 ''; 82 83 switch(editorName) 84 { 85 case 'gutenberg': 86 wp.data.dispatch( 'core/editor' ).resetBlocks([]); 87 88 var editedContent = wp.data.select( "core/editor" ).getEditedPostContent(); 89 var newBlocks = wp.blocks.parse( content ); 90 wp.data.dispatch( "core/editor" ).insertBlocks( newBlocks ); 91 92 return true; 93 case 'tinymce': 94 95 if(!!tinymce.activeEditor) 96 tinymce.activeEditor.setContent( content ) 97 else if(!!tinyMCE.activeEditor) 98 tinyMCE.activeEditor.setContent( content ) 99 100 $('#content').val($('#hidden_travel_editor').html()); 101 102 return true; 103 } 104 105 return false; 106 // }}} 107 } 108 109 function getWpEditorContent() 110 { 111 // {{{ 112 if(!window.editorName) 113 window.editorName = (!!wp.data)?'gutenberg': 114 (!!tinymce || !!tinyMCE)?'tinymce': 115 ''; 116 117 switch(editorName) 118 { 119 case 'gutenberg': 120 return wp.data.select('core/editor').getEditedPostContent(); 121 case 'tinymce': 122 if($('#content').attr('aria-hidden') === 'true') // text editor 123 return (window.tinymce || window.tinyMCE).activeEditor.getContent(); 124 else 125 return $('#content').val().replace(/\n/g, '<br/>'); 126 } 127 128 return ''; 129 // }}} 130 } 131 132 133 /** 134 * Finds the position of a keyWord 135 * 136 * @param {DomNode} e The Dom node representing the keyword, has to be a `<mark class='TANKeyWord'>` 137 * @param {jQueryNode} container The container in which the keyWords are. travel editor by default. 138 * 139 * @return {number} the position of this specific keyword, or 0 if not found 140 */ 141 142 function tan_domToNum(e, container=$('#travel_editor')) 143 { 144 // {{{ 145 list = container.find(' mark.TANKeyWord'); 146 for(var i=0 ; i<list.length ; i++) 147 if(list[i] === e) 148 return i; 149 return 0; 150 // }}} 151 } 152 153 /** 154 * function called when a keyWord is clicked 155 * 156 * @param {DomNode} e The clicked keyWord, has to be a `<mark class='TANKeyWord'>` 157 */ 158 function tan_clickMark(e) 159 { 160 tan_changeFocus(tan_domToNum(e)); 161 } 162 163 /** 164 * Toggle the specified keyWord on and off 165 * 166 * @param {number} foc The keyword to activate, the currently focussed keyword by default, pass -1 to toggle all, -2 to toggle all custom keywords 167 * @param {bool} flag Weather the keyWord should be activated or deactivated. Toggle by default 168 */ 169 function tan_activateKeyWord(foc = currentFocus, flag = undefined) 170 { 171 // {{{ 172 if(foc < 0) 173 { 174 let classes = ['TANKeyWord']; 175 if(foc === -2) 176 classes.push('custom'); 177 178 let classesStr = classes.map(v=>'.'+v).join(''); 179 180 f = $('#travel_editor mark.active'+classesStr).length 181 - $('#travel_editor mark'+classesStr+':not(.active)').length 182 183 184 f = f<0; 185 186 $('#travel_editor mark'+classesStr).each(function() { 187 tan_activateKeyWord(tan_domToNum(this), f); 188 }); 189 190 return; 191 } 192 193 elem = $('#travel_editor mark.TANKeyWord').eq(foc); 194 195 if(elem.hasClass('active') === flag) 196 return; 197 198 if(flag === undefined) 199 elem.toggleClass('active'); 200 else 201 elem.toggleClass('active', flag); 202 203 elem.attr('data-adv', $('#navMenu_merchantSelect').find('option:selected').val()); 204 205 206 if(foc === currentFocus) $('#navMenu_buttonOnOff').toggleClass('active', elem.hasClass('active')); 207 208 // }}} 209 } 210 211 var totalReplaced = 0; 212 213 /** 214 * Surround the keyWords by <mark>, to highlighting it 215 * 216 * @param {RegExp} target regular expression matching the targeted keywords 217 */ 218 jQuery.fn.tan_highlightKeywords = function(target) 219 { 220 // {{{ 221 // 222 // Get all text nodes: 223 // 224 excludeNodes = ['SCRIPT', 'STYLE', 'H1', 'H2', 'H3', 'H4']; 225 var $textNodes = this 226 .find("*") 227 .andSelf() 228 .contents() 229 .filter(function() { 230 return this.nodeType === 3 231 && !$(this).parent("a").length 232 && !$(this).parent(excludeNodes.join(',')).length 233 && !$(this).parent('.TANKeyWord').length 234 && !$(this).parent('.TAN_link').length 235 && $(this).text().trim() !== ''; 236 }); 237 238 // 239 // Iterate through the text nodes, 240 // highlighting the keyWords 241 // 242 243 let totalReplaced = 0; 244 $textNodes.each(function(index, element) { 245 246 match = $(element).text().match(target); 247 248 if(match === null) return; 249 var i=0; 250 var finalStr = $(element).text().replace(target, function(word) { 251 var originalWord = match[i]; 252 i++; 253 totalReplaced++; 254 255 let tmp = addedKeyWords.filter(v=>v[0].format() === originalWord.format()) 256 257 let c = ''; 258 let s = originalWord; 259 if(tmp.length) 260 { 261 c = ' custom' 262 s = tmp[0][1]; 263 } 264 265 return `<mark class='TANKeyWord`+c+`' 266 data-search='`+s+`' 267 data-adv=`+$('#navMenu_merchantSelect').find('option:selected').val()+`>` 268 + originalWord 269 + `</mark>`; 270 }); 271 272 $(element).replaceWith(finalStr); 273 }); 274 275 $('.TANKeyWord').each(function(i) { 276 277 this.onclick = function() { 278 tan_changeFocus(tan_domToNum(this)); 279 }; 280 }); 281 282 // }}} 283 } 284 285 /** 286 * copy content from the travel ads network editor to the tiny MCE 287 */ 288 function copy_travel2wp() 289 { 290 // {{{ 291 292 $('#hidden_travel_editor').html(($('div#travel_editor').html())); 293 294 $('#hidden_travel_editor mark.TANKeyWord').each(function() { 295 $(this).replaceWith(this.outerHTML.replace(/\bmark\b/, 'span')) 296 }); 297 298 $('#hidden_travel_editor span.TANKeyWord').each(function() { 299 $(this).removeClass('current'); 300 $(this).removeClass('TANKeyWord'); 301 302 $(this).addClass('TAN_link'); 303 304 if($(this).hasClass('custom')) 305 $(this).addClass('TAN_link_custom'); 306 }); 307 308 $('#hidden_travel_editor section.tan_asset').each(function(i) { 309 $(this).find('script').siblings().remove(); 310 }); 311 312 setWpEditorContent( $('#hidden_travel_editor').html() ); 313 314 $('div#travel_editor section.tan_asset script').remove(); 315 316 $('#hidden_travel_editor').empty(); 317 318 // }}} 319 } 320 321 /** 322 * Copy the content from the wordpress editor to the travel editor 323 * 324 * @param {string} content The content to put in the travel editor 325 */ 326 function copy_wp2travel() 327 { 328 // {{{ 329 content = getWpEditorContent(); 330 331 332 if( $('div.mce-tinymce').css('display') === 'none' // normal mode 333 && $('wp-editor-area').css('display') === 'none') // text mode 334 { 335 $('#travel_editor').html('<h1 style="text-align:center">The visual Editor is not supported for now...</h1>'+ 336 '<h2 style="text-align:center">You can switch to autmatic mode!</h2>'); 337 $('#navMenu').toggle(false); 338 $('#navMenuOffset').toggle(false); 339 return; 340 } 341 $('#navMenu').toggle(true); 342 $('#navMenuOffset').toggle(true); 343 344 totalReplaced = 0; 345 $('#travel_editor').empty(); 346 $('#travel_editor').html(content); 347 $('#travel_editor span.TAN_link').each(function() { 348 349 $(this).replaceWith(this.outerHTML.replace(/\bspan\b/, 'mark')) 350 }); 351 352 $('#travel_editor mark.TAN_link').each(function() { 353 $(this).removeClass('TAN_link_custom'); 354 $(this).removeClass('TAN_link'); 355 356 $(this).addClass('TANKeyWord'); 357 }); 358 359 360 $("#travel_editor").tan_highlightKeywords(createNewRegexp(keyWordsRegExp, addedKeyWords.map(v=>v[0]))); 361 362 tan_changeFocus(currentFocus, false); 363 $('#navMenu_totalKeyWord').text($('#travel_editor mark.TANKeyWord').length); 364 // }}} 365 } 366 367 /** 368 * Transforms the google places API response to a easier to use object 369 * 370 * @param {Object} googlePlace The object returned by the google places API 371 * -> ``` 372 * { 373 * address_coponents: <array of addresses>, 374 * geometry: <geometry, lat, long>, 375 * types: <array of types>, 376 * name: <name> 377 * } 378 * ``` 379 * 380 * @return {Object} An easier to use array 381 * -> ``` 382 * { 383 * country: <country name>, 384 * city: <city name>, 385 * route: <route name>, 386 * searchText: <text to search on the OTA's calls>, 387 * lat: <latitude>, 388 * lng: <longitude>, 389 * } 390 * 391 * ``` 392 */ 393 function googlePlaceFormat(googlePlace) 394 { 395 // {{{ 396 let out = {}; 397 398 let cityWrongNames = { 399 'Krung Thep Maha Nakhon': 'Bangkok', 400 'Krong Siem Reap': 'Siem Reap' 401 }; 402 403 let address = googlePlace.address_components.filter(v=> !/^[0-9]*$/.test(v.long_name) // exclude only numbers 404 && !/^[0-9]/.test(v.long_name)); // and beginning with number 405 406 let country = address.filter(v=>v.types.contains('country')), 407 administrativeAreas = address.filter(v=>v.types.some(t=>/administrative_area_level_\d+/.test(t))), 408 city = address.filter(v=>v.types.contains('locality', 'postal_town')), 409 subLocalAreas = address.filter(v=>v.types.some(t=>/sublocality_level_\d+/.test(t))), 410 route = address.filter(v=>v.types.contains('route')); 411 412 out = { 413 'lat': googlePlace.geometry.location.lat(), 414 'lng': googlePlace.geometry.location.lng() 415 }; 416 417 if(country.length) 418 { 419 out.country=country[0].long_name; 420 if(city.length) 421 { 422 out.city=city[0].long_name; 423 424 if(route.length) 425 out.route = route[0].long_name 426 else if(subLocalAreas.length) 427 out.route = subLocalAreas[0].long_name 428 } 429 else if(administrativeAreas.length) 430 out.city = administrativeAreas[0].long_name; 431 } 432 433 out.city = cityWrongNames[out.city] || out.city; 434 435 436 if(!googlePlace.types.contains('lodging', 'premise')) // is not lodging nor premise 437 out.searchText = (out.route||'')+', '+(out.city||'')+', '+(out.country||''); 438 else 439 out.searchText = (googlePlace.name||'')+', '+(out.city||'')+', '+(out.country||''); 440 441 out.searchText = out.searchText.replace(/^[, ]+/, ''); 442 443 return out; 444 // }}} 445 } 446 447 /** 448 * Adds the custom keyWords to the original regexp 449 * 450 * @param {RegExp} original The original regexp conaining all the default keywords 451 * @param {Array} keyWords A list of the new keyWords 452 */ 453 function createNewRegexp(original, keyWords) 454 { 455 // {{{ 456 // 457 // transforming the old regexp to string 458 // 459 let newRegExp = original.toString().replace(/^[^\/]*\/|\/[^/]*$/g, ''); 460 461 // 462 // adding the new keyWords 463 // 464 for(let i=0 ; i<keyWords.length ; i++) 465 { 466 newRegExp += '|'; 467 if(/^[a-z]/i.test(keyWords[i])) // begin with latin alphabet 468 newRegExp += '\\b'; 469 470 newRegExp += keyWords[i].replace(/([\[\]\(\)\+\/\*\?])/g, '\\$1'); // making the word regex proof 471 472 if(/[a-z]$/i.test(keyWords[i])) // finish with latin alphabet 473 newRegExp += '\\b'; 474 475 newRegExp += '|'; 476 if(/^[a-z]/i.test(keyWords[i])) // begin with latin alphabet 477 newRegExp += '\\b'; 478 479 newRegExp += keyWords[i].format().replace(/([\[\]\(\)\+\/\*\?])/g, '\\$1'); // making the word regex proof 480 481 if(/[a-z]$/i.test(keyWords[i])) // finish with latin alphabet 482 newRegExp += '\\b'; 483 484 } 485 486 return new RegExp(newRegExp, 'gi'); 487 // }}} 488 } 489 490 /** 491 * initialize the google places field 492 */ 493 function initAutocomplete() 494 { 495 // {{{ 496 // 497 // avoids to trigger the page reload on enter 498 // 499 $('#googleField').on('keydown', function(e){if(e.keyCode===13) e.preventDefault()}); 500 501 googleField = new google.maps.places.Autocomplete($('#googleField')[0], 502 {types: ['establishment', 'geocode']}); 503 504 googleField.setFields(['address_components', 505 'geometry', 506 'types', 507 'name']); 508 509 googleField.addListener('place_changed', fillInAddress); 510 // }}} 511 } 512 513 /** 514 * Callback, listen to google places field 515 */ 516 function fillInAddress() 517 { 518 // {{{ 519 var place = googleField.getPlace(); 520 521 $('#googleField').addClass('kwAdd'); 522 let kw = $('#googleField').val().split(/, ?/)[0] 523 524 if(lastSel[0]) 525 { 526 let start = $(lastSel[1].startContainer).parents('mark.TANKeyWord'); 527 let end = $(lastSel[1].endContainer).parents('mark.TANKeyWord'); 528 529 lastSel[1].deleteContents(); 530 lastSel[1].insertNode(document.createTextNode(' '+kw+' ')); 531 532 start.replaceWith(function() { return this.innerHTML }); 533 end.replaceWith(function() { return this.innerHTML }); 534 } 535 536 if( !addedKeyWords.map(v=>v[0].format()).contains(kw.format()) 537 && ( !kw.match(keyWordsRegExp) 538 || kw.match(keyWordsRegExp)[0] !== kw )) // kw not present in predefined keywords 539 addedKeyWords.push([kw, googlePlaceFormat(place).searchText]); 540 541 if(addedKeyWords.length) 542 $('#newKWs').html('<br/>Your keywords:<br/> '+addedKeyWords.map(v=>'<span class=newKW>'+v[0]+'</span>').join(' ')); 543 544 545 $('span.newKW').click(function() { 546 let kw = this.innerHTML; 547 548 addedKeyWords = addedKeyWords.filter(v=>v[0] !== kw); 549 $('.TANKeyWord').each(function() { 550 if(kw.format() === this.innerText.format()) 551 $(this).replaceWith(this.innerHTML); 552 }); 553 $(this).remove(); 554 }); 555 556 557 $("#travel_editor").tan_highlightKeywords(createNewRegexp(keyWordsRegExp, addedKeyWords.map(v=>v[0]))); 558 // }}} 559 } 560 561 // 562 // setting up the listeners 563 // 564 565 $('div#navMenu_buttonOnOff').click(function(){ 566 tan_activateKeyWord(); 567 }); 568 569 $('div[aria-label="TravelAdsNetwork Manual"]').click(function() { 570 // {{{ 571 if($('#travelads_sectionid').css('display') === 'none') 572 { 573 setTimeout(function() { 574 $('html, body').animate({ 575 scrollTop: $('#travelads_sectionid').offset().top - 100 576 }, 500); 577 }, 300); 578 } 579 // }}} 580 }); 581 582 $('#navMenu_merchantSelect').change(function() { 583 $('.TANKeyWord.current').attr('data-adv', $(this).find('option:selected').val()) 584 tan_activateKeyWord(undefined, true); 585 }); 586 587 $('#navMenuprevWord').click(function() { 588 tan_changeFocus(currentFocus-1); 589 }); 590 $('#navMenunextWord').click(function() { 591 tan_changeFocus(currentFocus+1); 592 }); 593 594 $('#publish').click(function() { 595 copy_travel2wp(); 596 }); 597 598 599 let stop=setInterval(function() { 600 // {{{ 601 if(!(window.tinyMCE || window.tinymce).editors) 602 return; 603 604 clearInterval(stop); 605 606 $('#travelads_sectionid').on('mouseenter', function() { 607 if(!travelFocused) 608 { 609 travelFocused = true; 610 copy_wp2travel(); 611 } 612 613 let editors = ['#content', '.editor-writing-flow'] 614 .concat([...(window.tinyMCE || window.tinymce).editors] 615 .map(v=>(!!v.container && '#'+v.container.getAttribute('id') || ''))) 616 .filter(v=>!!v) 617 .join(', '); 618 619 $(editors).each(function() { 620 this.onmouseenter = function() { 621 if(travelFocused) 622 copy_travel2wp(); 623 travelFocused = false; 624 }; 625 }); 626 }); 627 628 let content = getWpEditorContent(); 629 630 var parser = new DOMParser(); 631 data = parser.parseFromString('<html>'+content.replace(/ /g, '<br/>')+'</html>', "text/xml"); 632 633 [...data.getElementsByClassName('TAN_link_custom')].forEach(function(elem) { 634 let flag=-2; 635 addedKeyWords.forEach(function([kw], index) { 636 if(kw.format() === elem.innerHTML.format()) 637 if(kw.format() !== kw) 638 flag = index; 639 else 640 flag = -1; 641 }); 642 643 switch(flag) 644 { 645 case -2: 646 addedKeyWords.push([elem.innerHTML, elem.getAttribute('data-search')]); 647 break; 648 case -1: 649 break; 650 default: 651 addedKeyWords[flag] = [elem.innerHTML, elem.getAttribute('data-search')]; 652 } 653 }); 654 655 if(addedKeyWords.length) 656 $('#newKWs').html('<br/>Your keywords:<br/> '+addedKeyWords.map(v=>'<span class=newKW>'+v[0]+'</span>').join(', ')); 657 // }}} 658 }, 500); 659 660 661 let ctrlDown = false; 662 let shiftDown = false; 663 let lastSel = []; 664 665 /** 666 * Fast mode, allow to go through the keywords 667 * with the keyboard, much faster than the mouse 668 */ 669 $('#navMenu_keyWord').on('keydown', function(e) { 670 // {{{ 671 let code = e.keyCode; 672 let sel = $("#navMenu_merchantSelect"); 673 switch(code) 674 { 675 case 39: // right 676 tan_changeFocus(currentFocus+1); 677 break; 678 case 37: 679 tan_changeFocus(currentFocus-1); 680 break; // left 681 case 38: // up 682 sel.val((sel.find(':selected').prev('option')[0] || sel.find('option:last-child')[0]).value); 683 sel.trigger('change'); 684 break; 685 case 40: // down 686 sel.val((sel.find(':selected + option')[0] || sel.find('option:first-child')[0]).value); 687 sel.trigger('change'); 688 break; 689 case 32: tan_activateKeyWord(ctrlDown?-1:shiftDown?-2:currentFocus); break; // space 690 case 13: tan_activateKeyWord(ctrlDown?-1:shiftDown?-2:currentFocus); break; // enter 691 692 case 17: ctrlDown=true; break; 693 case 16: shiftDown=true; break; 694 } 695 696 e.preventDefault(); 697 // }}} 698 }) 699 $('#navMenu_keyWord').on('keyup', function(e) { 700 // {{{ 701 if(e.keyCode === 17) 702 ctrlDown=false; 703 if(e.keyCode === 16) 704 shiftDown=false; 705 // }}} 706 }); 707 708 $('#travel_editor, #navMenu tr:last-child').on('mouseup', function(e) { 709 // {{{ 710 setTimeout(function() { 711 if( !$(e.target).is("#navMenu_merchantSelect") 712 && !getSelectionHtml()[0] 713 && !$('#googleField').is(':focus')) 714 $('#navMenu_keyWord')[0].focus(); 715 }, 100); 716 // }}} 717 }) 718 719 $('#travel_editor').on('mouseup', function(e) { 720 // {{{ 721 let sel = getSelectionHtml(); 722 if(!sel[0]) 723 return; 724 725 lastSel = sel; 726 $(lastSel[1]).parents('mark.TANKeyWord').replaceWith(function() { 727 return this.innerHTML; 728 }) 729 sel = sel[0]; 730 731 $('#googleField').val(sel.replace(/<[^>]*>/g, '')); 732 $('#googleField')[0].focus(); 733 // }}} 734 }); 735 736 document.addEventListener('scroll', function (e) { 737 // {{{ 738 739 let topOffset = $('#wpadminbar').height() + 740 (window.editorName==='gutenberg'?$('.edit-post-header').outerHeight():0) + 741 ((tmp=$('.components-notice')).length?[...tmp].reduce((a,v)=>a+$(v).outerHeight(),0):0), 742 diff = $(window).scrollTop() - $('#travelads_sectionid .inside').offset().top + topOffset; 743 744 if(diff >= 0) 745 { 746 $('#navMenu').toggleClass('flying', true); 747 748 $('#navMenu').css({ 749 position: 'fixed', 750 left: $('#travelads_sectionid').offset().left, 751 top: topOffset, 752 753 width: $('#travelads_sectionid').width()+2, 754 }); 755 756 $('.pac-container.pac-logo').css({ 757 top: $('#googleField').offset().top + $('#googleField').height() + 8, 758 }); 759 760 } 761 else 762 { 763 $('#navMenu').toggleClass('flying', false); 764 $('#navMenu').css({ 765 position: '', 766 left: '', 767 top: '', 768 width: '' 769 }); 770 } 771 // }}} 772 }, true); 773 774 initAutocomplete(); 775 776 777 // 778 // creation of the Regex (list of keywords) and initializations 779 // 780 data=keyWords; 781 var to_replace = ''; 782 for(var i=0 ; i<data.length ; i++) 783 to_replace += '\\b'+data[i]+'\\b|'; 784 785 to_replace = to_replace.slice(0, -1); 786 787 keyWordsRegExp = new RegExp(to_replace, 'gi') 788 789 790 // 791 // Open the dialog by default 792 // 793 setTimeout(function() { 794 $('#travelads_sectionid.closed button').click(); 795 }, 2000); 796 797 // }}} 798 }); 785 799 }(jQuery); -
traveladsnetwork-com/tags/1.0/readme.txt
r2083453 r2083624 2 2 Contributors: christophesecher 3 3 Tags: travel, affiliate plugin, affiliate link, deeplinking, deeplinks, travel booking, bloggers, merchant, travel content, travel, Travel Ads Network, Traveladsnetwork, content monetization, creating purchase intent, monetize your audience data, Convert Content into Revenue 4 Tags: travel affiliate plugin affiliate link deeplinking deeplinks travel booking bloggers merchant travel content travel Travel Ads Network Traveladsnetwork content monetization creating purchase intent monetize your audience data Convert Content into Revenue5 4 Requires at least: 4.9.0 6 Tested up to: 4.9.85 Tested up to: 5.2 7 6 Requires PHP: 7.3 8 7 Stable tag: trunk -
traveladsnetwork-com/trunk/assets/css/pageEdit.css
r2083453 r2083624 20 20 table#navMenu 21 21 { 22 box-sizing: border-box; 22 23 width:100%; 23 24 position:absolute; … … 28 29 padding:0 20px; 29 30 z-index: 1; 31 32 transition: border-radius .3s, 33 box-shadow .3s; 34 } 35 table#navMenu.flying { 36 border-radius: 0 0 10px 10px; 37 box-shadow: 0px 5px 10px 5px #0003; 30 38 } 31 39 table#navMenu td -
traveladsnetwork-com/trunk/assets/js/pageEdit.js
r2083453 r2083624 15 15 16 16 !function($) { 17 $(document).ready(function() { 18 // {{{ 19 20 /** 21 * Changes the current focussed word 22 * 23 * @param {int} nb The keyWord to highlight -> e.g. if you want to highlight the 5th keyword, nb=4 24 * @param {bool} scroll Should the screen scroll automatically to this keyword? 25 */ 26 function tan_changeFocus(nb=0, scroll=true) 27 { 28 // {{{ 29 nb = nb || 0; 30 31 nb = (nb+$('#travel_editor mark.TANKeyWord').length)%$('#travel_editor mark.TANKeyWord').length 32 33 currentFocus = nb; 34 elem = $('#travel_editor mark.TANKeyWord')[nb]; 35 36 $('#navMenu_keyWord').val(elem.innerHTML.trim()); 37 38 $('#travel_editor mark.TANKeyWord').removeClass('current'); 39 $(elem).addClass('current'); 40 41 $('#navMenu_currentKeyWord').text(nb+1); 42 43 if($(elem).hasClass('active')) 44 { 45 $('div#navMenu_buttonOnOff').addClass('active'); 46 } 47 else 48 { 49 $('div#navMenu_buttonOnOff').removeClass('active'); 50 } 51 52 53 $('select#navMenu_merchantSelect').val($(elem).attr('data-adv') || $('select#navMenu_merchantSelect option')[0].value); 54 55 if(scroll) 56 { 57 $('html, body').clearQueue(); 58 $('html, body').animate({ 59 scrollTop: $(elem).offset().top-$(window).height()/2 60 }, 500); 61 } 62 // }}} 63 } 64 65 function setWpEditorContent(content) 66 { 67 // {{{ 68 if(!window.editorName) 69 window.editorName = (!!wp.data)?'gutenberg': 70 (!!tinymce || !!tinyMCE)?'tinymce': 71 ''; 72 73 switch(editorName) 74 { 75 case 'gutenberg': 76 wp.data.dispatch( 'core/editor' ).resetBlocks([]); 77 78 var editedContent = wp.data.select( "core/editor" ).getEditedPostContent(); 79 var newBlocks = wp.blocks.parse( content ); 80 wp.data.dispatch( "core/editor" ).insertBlocks( newBlocks ); 81 82 return true; 83 case 'tinymce': 84 85 if(!!tinymce.activeEditor) 86 tinymce.activeEditor.setContent( content ) 87 else if(!!tinyMCE.activeEditor) 88 tinyMCE.activeEditor.setContent( content ) 89 90 $('#content').val($('#hidden_travel_editor').html()); 91 92 return true; 93 } 94 95 return false; 96 // }}} 97 } 98 99 function getWpEditorContent() 100 { 101 // {{{ 102 if(!window.editorName) 103 window.editorName = (!!wp.data)?'gutenberg': 104 (!!tinymce || !!tinyMCE)?'tinymce': 105 ''; 106 107 switch(editorName) 108 { 109 case 'gutenberg': 110 return wp.data.select('core/editor').getEditedPostContent(); 111 case 'tinymce': 112 if($('#content').attr('aria-hidden') === 'true') // text editor 113 return (window.tinymce || window.tinyMCE).activeEditor.getContent(); 114 else 115 return $('#content').val().replace(/\n/g, '<br/>'); 116 } 117 118 return ''; 119 // }}} 120 } 121 122 123 /** 124 * Finds the position of a keyWord 125 * 126 * @param {DomNode} e The Dom node representing the keyword, has to be a `<mark class='TANKeyWord'>` 127 * @param {jQueryNode} container The container in which the keyWords are. travel editor by default. 128 * 129 * @return {number} the position of this specific keyword, or 0 if not found 130 */ 131 132 function tan_domToNum(e, container=$('#travel_editor')) 133 { 134 // {{{ 135 list = container.find(' mark.TANKeyWord'); 136 for(var i=0 ; i<list.length ; i++) 137 if(list[i] === e) 138 return i; 139 return 0; 140 // }}} 141 } 142 143 /** 144 * function called when a keyWord is clicked 145 * 146 * @param {DomNode} e The clicked keyWord, has to be a `<mark class='TANKeyWord'>` 147 */ 148 function tan_clickMark(e) 149 { 150 tan_changeFocus(tan_domToNum(e)); 151 } 152 153 /** 154 * Toggle the specified keyWord on and off 155 * 156 * @param {number} foc The keyword to activate, the currently focussed keyword by default, pass -1 to toggle all, -2 to toggle all custom keywords 157 * @param {bool} flag Weather the keyWord should be activated or deactivated. Toggle by default 158 */ 159 function tan_activateKeyWord(foc = currentFocus, flag = undefined) 160 { 161 // {{{ 162 if(foc < 0) 163 { 164 let classes = ['TANKeyWord']; 165 if(foc === -2) 166 classes.push('custom'); 167 168 let classesStr = classes.map(v=>'.'+v).join(''); 169 170 f = $('#travel_editor mark.active'+classesStr).length 171 - $('#travel_editor mark'+classesStr+':not(.active)').length 172 173 174 f = f<0; 175 176 $('#travel_editor mark'+classesStr).each(function() { 177 tan_activateKeyWord(tan_domToNum(this), f); 178 }); 179 180 return; 181 } 182 183 elem = $('#travel_editor mark.TANKeyWord').eq(foc); 184 185 if(elem.hasClass('active') === flag) 186 return; 187 188 if(flag === undefined) 189 elem.toggleClass('active'); 190 else 191 elem.toggleClass('active', flag); 192 193 elem.attr('data-adv', $('#navMenu_merchantSelect').find('option:selected').val()); 194 195 196 if(foc === currentFocus) $('#navMenu_buttonOnOff').toggleClass('active', elem.hasClass('active')); 197 198 // }}} 199 } 200 201 var totalReplaced = 0; 202 203 /** 204 * Surround the keyWords by <mark>, to highlighting it 205 * 206 * @param {RegExp} target regular expression matching the targeted keywords 207 */ 208 jQuery.fn.tan_highlightKeywords = function(target) 209 { 210 // {{{ 211 // 212 // Get all text nodes: 213 // 214 excludeNodes = ['SCRIPT', 'STYLE', 'H1', 'H2', 'H3', 'H4']; 215 var $textNodes = this 216 .find("*") 217 .andSelf() 218 .contents() 219 .filter(function() { 220 return this.nodeType === 3 221 && !$(this).parent("a").length 222 && !$(this).parent(excludeNodes.join(',')).length 223 && !$(this).parent('.TANKeyWord').length 224 && !$(this).parent('.TAN_link').length 225 && $(this).text().trim() !== ''; 226 }); 227 228 // 229 // Iterate through the text nodes, 230 // highlighting the keyWords 231 // 232 233 let totalReplaced = 0; 234 $textNodes.each(function(index, element) { 235 236 match = $(element).text().match(target); 237 238 if(match === null) return; 239 var i=0; 240 var finalStr = $(element).text().replace(target, function(word) { 241 var originalWord = match[i]; 242 i++; 243 totalReplaced++; 244 245 let tmp = addedKeyWords.filter(v=>v[0].format() === originalWord.format()) 246 247 let c = ''; 248 let s = originalWord; 249 if(tmp.length) 250 { 251 c = ' custom' 252 s = tmp[0][1]; 253 } 254 255 return `<mark class='TANKeyWord`+c+`' 256 data-search='`+s+`' 257 data-adv=`+$('#navMenu_merchantSelect').find('option:selected').val()+`>` 258 + originalWord 259 + `</mark>`; 260 }); 261 262 $(element).replaceWith(finalStr); 263 }); 264 265 $('.TANKeyWord').each(function(i) { 266 267 this.onclick = function() { 268 tan_changeFocus(tan_domToNum(this)); 269 }; 270 }); 271 272 // }}} 273 } 274 275 /** 276 * copy content from the travel ads network editor to the tiny MCE 277 */ 278 function copy_travel2wp() 279 { 280 // {{{ 281 282 $('#hidden_travel_editor').html(($('div#travel_editor').html())); 283 284 $('#hidden_travel_editor mark.TANKeyWord').each(function() { 285 $(this).replaceWith(this.outerHTML.replace(/\bmark\b/, 'span')) 286 }); 287 288 $('#hidden_travel_editor span.TANKeyWord').each(function() { 289 $(this).removeClass('current'); 290 $(this).removeClass('TANKeyWord'); 291 292 $(this).addClass('TAN_link'); 293 294 if($(this).hasClass('custom')) 295 $(this).addClass('TAN_link_custom'); 296 }); 297 298 $('#hidden_travel_editor section.tan_asset').each(function(i) { 299 $(this).find('script').siblings().remove(); 300 }); 301 302 setWpEditorContent( $('#hidden_travel_editor').html() ); 303 304 $('div#travel_editor section.tan_asset script').remove(); 305 306 $('#hidden_travel_editor').empty(); 307 308 // }}} 309 } 310 311 /** 312 * Copy the content from the wordpress editor to the travel editor 313 * 314 * @param {string} content The content to put in the travel editor 315 */ 316 function copy_wp2travel() 317 { 318 // {{{ 319 content = getWpEditorContent(); 320 321 322 if( $('div.mce-tinymce').css('display') === 'none' // normal mode 323 && $('wp-editor-area').css('display') === 'none') // text mode 324 { 325 $('#travel_editor').html('<h1 style="text-align:center">The visual Editor is not supported for now...</h1>'+ 326 '<h2 style="text-align:center">You can switch to autmatic mode!</h2>'); 327 $('#navMenu').toggle(false); 328 $('#navMenuOffset').toggle(false); 329 return; 330 } 331 $('#navMenu').toggle(true); 332 $('#navMenuOffset').toggle(true); 333 334 totalReplaced = 0; 335 $('#travel_editor').empty(); 336 $('#travel_editor').html(content); 337 $('#travel_editor span.TAN_link').each(function() { 338 339 $(this).replaceWith(this.outerHTML.replace(/\bspan\b/, 'mark')) 340 }); 341 342 $('#travel_editor mark.TAN_link').each(function() { 343 $(this).removeClass('TAN_link_custom'); 344 $(this).removeClass('TAN_link'); 345 346 $(this).addClass('TANKeyWord'); 347 }); 348 349 350 $("#travel_editor").tan_highlightKeywords(createNewRegexp(keyWordsRegExp, addedKeyWords.map(v=>v[0]))); 351 352 console.log('here'); 353 tan_changeFocus(currentFocus, false); 354 console.log('not here'); 355 $('#navMenu_totalKeyWord').text($('#travel_editor mark.TANKeyWord').length); 356 // }}} 357 } 358 359 /** 360 * Transforms the google places API response to a easier to use object 361 * 362 * @param {Object} googlePlace The object returned by the google places API 363 * -> ``` 364 * { 365 * address_coponents: <array of addresses>, 366 * geometry: <geometry, lat, long>, 367 * types: <array of types>, 368 * name: <name> 369 * } 370 * ``` 371 * 372 * @return {Object} An easier to use array 373 * -> ``` 374 * { 375 * country: <country name>, 376 * city: <city name>, 377 * route: <route name>, 378 * searchText: <text to search on the OTA's calls>, 379 * lat: <latitude>, 380 * lng: <longitude>, 381 * } 382 * 383 * ``` 384 */ 385 function googlePlaceFormat(googlePlace) 386 { 387 // {{{ 388 let out = {}; 389 390 let cityWrongNames = { 391 'Krung Thep Maha Nakhon': 'Bangkok', 392 'Krong Siem Reap': 'Siem Reap' 393 }; 394 395 let address = googlePlace.address_components.filter(v=> !/^[0-9]*$/.test(v.long_name) // exclude only numbers 396 && !/^[0-9]/.test(v.long_name)); // and beginning with number 397 398 let country = address.filter(v=>v.types.contains('country')), 399 administrativeAreas = address.filter(v=>v.types.some(t=>/administrative_area_level_\d+/.test(t))), 400 city = address.filter(v=>v.types.contains('locality', 'postal_town')), 401 subLocalAreas = address.filter(v=>v.types.some(t=>/sublocality_level_\d+/.test(t))), 402 route = address.filter(v=>v.types.contains('route')); 403 404 out = { 405 'lat': googlePlace.geometry.location.lat(), 406 'lng': googlePlace.geometry.location.lng() 407 }; 408 409 if(country.length) 410 { 411 out.country=country[0].long_name; 412 if(city.length) 413 { 414 out.city=city[0].long_name; 415 416 if(route.length) 417 out.route = route[0].long_name 418 else if(subLocalAreas.length) 419 out.route = subLocalAreas[0].long_name 420 } 421 else if(administrativeAreas.length) 422 out.city = administrativeAreas[0].long_name; 423 } 424 425 out.city = cityWrongNames[out.city] || out.city; 426 427 428 if(!googlePlace.types.contains('lodging', 'premise')) // is not lodging nor premise 429 out.searchText = (out.route||'')+', '+(out.city||'')+', '+(out.country||''); 430 else 431 out.searchText = (googlePlace.name||'')+', '+(out.city||'')+', '+(out.country||''); 432 433 out.searchText = out.searchText.replace(/^[, ]+/, ''); 434 435 return out; 436 // }}} 437 } 438 439 /** 440 * Adds the custom keyWords to the original regexp 441 * 442 * @param {RegExp} original The original regexp conaining all the default keywords 443 * @param {Array} keyWords A list of the new keyWords 444 */ 445 function createNewRegexp(original, keyWords) 446 { 447 // {{{ 448 // 449 // transforming the old regexp to string 450 // 451 let newRegExp = original.toString().replace(/^[^\/]*\/|\/[^/]*$/g, ''); 452 453 // 454 // adding the new keyWords 455 // 456 for(let i=0 ; i<keyWords.length ; i++) 457 { 458 newRegExp += '|'; 459 if(/^[a-z]/i.test(keyWords[i])) // begin with latin alphabet 460 newRegExp += '\\b'; 461 462 newRegExp += keyWords[i].replace(/([\[\]\(\)\+\/\*\?])/g, '\\$1'); // making the word regex proof 463 464 if(/[a-z]$/i.test(keyWords[i])) // finish with latin alphabet 465 newRegExp += '\\b'; 466 467 newRegExp += '|'; 468 if(/^[a-z]/i.test(keyWords[i])) // begin with latin alphabet 469 newRegExp += '\\b'; 470 471 newRegExp += keyWords[i].format().replace(/([\[\]\(\)\+\/\*\?])/g, '\\$1'); // making the word regex proof 472 473 if(/[a-z]$/i.test(keyWords[i])) // finish with latin alphabet 474 newRegExp += '\\b'; 475 476 } 477 478 return new RegExp(newRegExp, 'gi'); 479 // }}} 480 } 481 482 /** 483 * initialize the google places field 484 */ 485 function initAutocomplete() 486 { 487 // {{{ 488 // 489 // avoids to trigger the page reload on enter 490 // 491 $('#googleField').on('keydown', function(e){if(e.keyCode===13) e.preventDefault()}); 492 493 googleField = new google.maps.places.Autocomplete($('#googleField')[0], 494 {types: ['establishment', 'geocode']}); 495 496 googleField.setFields(['address_components', 497 'geometry', 498 'types', 499 'name']); 500 501 googleField.addListener('place_changed', fillInAddress); 502 // }}} 503 } 504 505 /** 506 * Callback, listen to google places field 507 */ 508 function fillInAddress() 509 { 510 // {{{ 511 var place = googleField.getPlace(); 512 513 $('#googleField').addClass('kwAdd'); 514 let kw = $('#googleField').val().split(/, ?/)[0] 515 516 if(lastSel[0]) 517 { 518 let start = $(lastSel[1].startContainer).parents('mark.TANKeyWord'); 519 let end = $(lastSel[1].endContainer).parents('mark.TANKeyWord'); 520 521 lastSel[1].deleteContents(); 522 lastSel[1].insertNode(document.createTextNode(' '+kw+' ')); 523 524 start.replaceWith(function() { return this.innerHTML }); 525 end.replaceWith(function() { return this.innerHTML }); 526 } 527 528 if( !addedKeyWords.map(v=>v[0].format()).contains(kw.format()) 529 && ( !kw.match(keyWordsRegExp) 530 || kw.match(keyWordsRegExp)[0] !== kw )) // kw not present in predefined keywords 531 addedKeyWords.push([kw, googlePlaceFormat(place).searchText]); 532 533 if(addedKeyWords.length) 534 $('#newKWs').html('<br/>Your keywords:<br/> '+addedKeyWords.map(v=>'<span class=newKW>'+v[0]+'</span>').join(' ')); 535 536 537 $('span.newKW').click(function() { 538 let kw = this.innerHTML; 539 540 addedKeyWords = addedKeyWords.filter(v=>v[0] !== kw); 541 $('.TANKeyWord').each(function() { 542 if(kw.format() === this.innerText.format()) 543 $(this).replaceWith(this.innerHTML); 544 }); 545 $(this).remove(); 546 }); 547 548 549 $("#travel_editor").tan_highlightKeywords(createNewRegexp(keyWordsRegExp, addedKeyWords.map(v=>v[0]))); 550 // }}} 551 } 552 553 // 554 // setting up the listeners 555 // 556 557 $('div#navMenu_buttonOnOff').click(function(){ 558 tan_activateKeyWord(); 559 }); 560 561 $('div[aria-label="TravelAdsNetwork Manual"]').click(function() { 562 // {{{ 563 if($('#travelads_sectionid').css('display') === 'none') 564 { 565 setTimeout(function() { 566 $('html, body').animate({ 567 scrollTop: $('#travelads_sectionid').offset().top - 100 568 }, 500); 569 }, 300); 570 } 571 // }}} 572 }); 573 574 $('#navMenu_merchantSelect').change(function() { 575 $('.TANKeyWord.current').attr('data-adv', $(this).find('option:selected').val()) 576 tan_activateKeyWord(undefined, true); 577 }); 578 579 $('#navMenuprevWord').click(function() { 580 tan_changeFocus(currentFocus-1); 581 }); 582 $('#navMenunextWord').click(function() { 583 tan_changeFocus(currentFocus+1); 584 }); 585 586 $('#publish').click(function() { 587 copy_travel2wp(); 588 }); 589 590 591 let stop=setInterval(function() { 592 // {{{ 593 if(!(window.tinyMCE || window.tinymce).editors) 594 return; 595 596 clearInterval(stop); 597 598 $('#travelads_sectionid').on('mouseenter', function() { 599 if(!travelFocused) 600 { 601 travelFocused = true; 602 copy_wp2travel(); 603 } 604 605 let editors = ['#content', '.editor-writing-flow'] 606 .concat([...(window.tinyMCE || window.tinymce).editors] 607 .map(v=>(!!v.container && '#'+v.container.getAttribute('id') || ''))) 608 .filter(v=>!!v) 609 .join(', '); 610 611 $(editors).each(function() { 612 this.onmouseenter = function() { 613 if(travelFocused) 614 copy_travel2wp(); 615 travelFocused = false; 616 }; 617 }); 618 }); 619 620 let content = getWpEditorContent(); 621 622 var parser = new DOMParser(); 623 data = parser.parseFromString('<html>'+content.replace(/ /g, '<br/>')+'</html>', "text/xml"); 624 625 [...data.getElementsByClassName('TAN_link_custom')].forEach(function(elem) { 626 let flag=-2; 627 addedKeyWords.forEach(function([kw], index) { 628 if(kw.format() === elem.innerHTML.format()) 629 if(kw.format() !== kw) 630 flag = index; 631 else 632 flag = -1; 633 }); 634 635 switch(flag) 636 { 637 case -2: 638 addedKeyWords.push([elem.innerHTML, elem.getAttribute('data-search')]); 639 break; 640 case -1: 641 break; 642 default: 643 addedKeyWords[flag] = [elem.innerHTML, elem.getAttribute('data-search')]; 644 } 645 }); 646 647 if(addedKeyWords.length) 648 $('#newKWs').html('<br/>Your keywords:<br/> '+addedKeyWords.map(v=>'<span class=newKW>'+v[0]+'</span>').join(', ')); 649 // }}} 650 }, 500); 651 652 653 let ctrlDown = false; 654 let shiftDown = false; 655 let lastSel = []; 656 657 /** 658 * Fast mode, allow to go through the keywords 659 * with the keyboard, much faster than the mouse 660 */ 661 $('#navMenu_keyWord').on('keydown', function(e) { 662 // {{{ 663 let code = e.keyCode; 664 let sel = $("#navMenu_merchantSelect"); 665 switch(code) 666 { 667 case 39: // right 668 tan_changeFocus(currentFocus+1); 669 break; 670 case 37: 671 tan_changeFocus(currentFocus-1); 672 break; // left 673 case 38: // up 674 sel.val((sel.find(':selected').prev('option')[0] || sel.find('option:last-child')[0]).value); 675 sel.trigger('change'); 676 break; 677 case 40: // down 678 sel.val((sel.find(':selected + option')[0] || sel.find('option:first-child')[0]).value); 679 sel.trigger('change'); 680 break; 681 case 32: tan_activateKeyWord(ctrlDown?-1:shiftDown?-2:currentFocus); break; // space 682 case 13: tan_activateKeyWord(ctrlDown?-1:shiftDown?-2:currentFocus); break; // enter 683 684 case 17: ctrlDown=true; break; 685 case 16: shiftDown=true; break; 686 } 687 688 e.preventDefault(); 689 // }}} 690 }) 691 $('#navMenu_keyWord').on('keyup', function(e) { 692 // {{{ 693 if(e.keyCode === 17) 694 ctrlDown=false; 695 if(e.keyCode === 16) 696 shiftDown=false; 697 // }}} 698 }); 699 700 $('#travel_editor, #navMenu tr:last-child').on('mouseup', function(e) { 701 // {{{ 702 setTimeout(function() { 703 if( !$(e.target).is("#navMenu_merchantSelect") 704 && !getSelectionHtml()[0] 705 && !$('#googleField').is(':focus')) 706 $('#navMenu_keyWord')[0].focus(); 707 }, 100); 708 // }}} 709 }) 710 711 $('#travel_editor').on('mouseup', function(e) { 712 // {{{ 713 let sel = getSelectionHtml(); 714 if(!sel[0]) 715 return; 716 717 lastSel = sel; 718 $(lastSel[1]).parents('mark.TANKeyWord').replaceWith(function() { 719 console.log('this', this); 720 return this.innerHTML; 721 }) 722 sel = sel[0]; 723 724 $('#googleField').val(sel.replace(/<[^>]*>/g, '')); 725 $('#googleField')[0].focus(); 726 // }}} 727 }); 728 729 document.addEventListener('scroll', function (e) { 730 // {{{ 731 let diff = $(window).scrollTop() - $('#travelads_sectionid .inside').offset().top + 32; 732 733 if(diff >= 0) 734 { 735 $('#navMenu').css({ 736 position: 'fixed', 737 left: $('#travelads_sectionid').offset().left, 738 top: $('#wpadminbar').height(), 739 740 width: $('#travelads_sectionid').width()+2, 741 }); 742 743 $('.pac-container.pac-logo').css({ 744 top: $('#googleField').offset().top + $('#googleField').height() + 8, 745 }); 746 747 } 748 else 749 { 750 $('#navMenu').css({ 751 position: '', 752 left: '', 753 top: '', 754 width: '' 755 }); 756 } 757 // }}} 758 }, true); 759 760 initAutocomplete(); 761 762 763 // 764 // creation of the Regex (list of keywords) and initializations 765 // 766 data=keyWords; 767 var to_replace = ''; 768 for(var i=0 ; i<data.length ; i++) 769 to_replace += '\\b'+data[i]+'\\b|'; 770 771 to_replace = to_replace.slice(0, -1); 772 773 keyWordsRegExp = new RegExp(to_replace, 'gi') 774 775 776 // 777 // Open the dialog by default 778 // 779 setTimeout(function() { 780 $('#travelads_sectionid.closed button').click(); 781 }, 2000); 782 783 // }}} 784 }); 17 $(document).ready(function() { 18 // {{{ 19 20 /** 21 * Changes the current focussed word 22 * 23 * @param {int} nb The keyWord to highlight -> e.g. if you want to highlight the 5th keyword, nb=4 24 * @param {bool} scroll Should the screen scroll automatically to this keyword? 25 */ 26 function tan_changeFocus(nb=0, scroll=true) 27 { 28 // {{{ 29 nb = nb || 0; 30 31 nb = (nb+$('#travel_editor mark.TANKeyWord').length)%$('#travel_editor mark.TANKeyWord').length 32 33 currentFocus = nb; 34 elem = $('#travel_editor mark.TANKeyWord')[nb]; 35 36 $('#navMenu_keyWord').val(elem.innerHTML.trim()); 37 38 $('#travel_editor mark.TANKeyWord').removeClass('current'); 39 $(elem).addClass('current'); 40 41 $('#navMenu_currentKeyWord').text(nb+1); 42 43 if($(elem).hasClass('active')) 44 { 45 $('div#navMenu_buttonOnOff').addClass('active'); 46 } 47 else 48 { 49 $('div#navMenu_buttonOnOff').removeClass('active'); 50 } 51 52 53 $('select#navMenu_merchantSelect').val($(elem).attr('data-adv') || $('select#navMenu_merchantSelect option')[0].value); 54 55 if(scroll) 56 { 57 switch( window.editorName ) { 58 case 'gutenberg': 59 $('.edit-post-layout__content').clearQueue(); 60 $('.edit-post-layout__content').animate({ 61 scrollTop: $(elem).offset().top-$('.edit-post-visual-editor').offset().top-$(window).height()/2 62 }, 500); 63 break; 64 case 'tinymce': 65 $('html, body').clearQueue(); 66 $('html, body').animate({ 67 scrollTop: $(elem).offset().top-$(window).height()/2 68 }, 500); 69 break; 70 } 71 } 72 // }}} 73 } 74 75 function setWpEditorContent(content) 76 { 77 // {{{ 78 if(!window.editorName) 79 window.editorName = (!!wp.data)?'gutenberg': 80 (!!tinymce || !!tinyMCE)?'tinymce': 81 ''; 82 83 switch(editorName) 84 { 85 case 'gutenberg': 86 wp.data.dispatch( 'core/editor' ).resetBlocks([]); 87 88 var editedContent = wp.data.select( "core/editor" ).getEditedPostContent(); 89 var newBlocks = wp.blocks.parse( content ); 90 wp.data.dispatch( "core/editor" ).insertBlocks( newBlocks ); 91 92 return true; 93 case 'tinymce': 94 95 if(!!tinymce.activeEditor) 96 tinymce.activeEditor.setContent( content ) 97 else if(!!tinyMCE.activeEditor) 98 tinyMCE.activeEditor.setContent( content ) 99 100 $('#content').val($('#hidden_travel_editor').html()); 101 102 return true; 103 } 104 105 return false; 106 // }}} 107 } 108 109 function getWpEditorContent() 110 { 111 // {{{ 112 if(!window.editorName) 113 window.editorName = (!!wp.data)?'gutenberg': 114 (!!tinymce || !!tinyMCE)?'tinymce': 115 ''; 116 117 switch(editorName) 118 { 119 case 'gutenberg': 120 return wp.data.select('core/editor').getEditedPostContent(); 121 case 'tinymce': 122 if($('#content').attr('aria-hidden') === 'true') // text editor 123 return (window.tinymce || window.tinyMCE).activeEditor.getContent(); 124 else 125 return $('#content').val().replace(/\n/g, '<br/>'); 126 } 127 128 return ''; 129 // }}} 130 } 131 132 133 /** 134 * Finds the position of a keyWord 135 * 136 * @param {DomNode} e The Dom node representing the keyword, has to be a `<mark class='TANKeyWord'>` 137 * @param {jQueryNode} container The container in which the keyWords are. travel editor by default. 138 * 139 * @return {number} the position of this specific keyword, or 0 if not found 140 */ 141 142 function tan_domToNum(e, container=$('#travel_editor')) 143 { 144 // {{{ 145 list = container.find(' mark.TANKeyWord'); 146 for(var i=0 ; i<list.length ; i++) 147 if(list[i] === e) 148 return i; 149 return 0; 150 // }}} 151 } 152 153 /** 154 * function called when a keyWord is clicked 155 * 156 * @param {DomNode} e The clicked keyWord, has to be a `<mark class='TANKeyWord'>` 157 */ 158 function tan_clickMark(e) 159 { 160 tan_changeFocus(tan_domToNum(e)); 161 } 162 163 /** 164 * Toggle the specified keyWord on and off 165 * 166 * @param {number} foc The keyword to activate, the currently focussed keyword by default, pass -1 to toggle all, -2 to toggle all custom keywords 167 * @param {bool} flag Weather the keyWord should be activated or deactivated. Toggle by default 168 */ 169 function tan_activateKeyWord(foc = currentFocus, flag = undefined) 170 { 171 // {{{ 172 if(foc < 0) 173 { 174 let classes = ['TANKeyWord']; 175 if(foc === -2) 176 classes.push('custom'); 177 178 let classesStr = classes.map(v=>'.'+v).join(''); 179 180 f = $('#travel_editor mark.active'+classesStr).length 181 - $('#travel_editor mark'+classesStr+':not(.active)').length 182 183 184 f = f<0; 185 186 $('#travel_editor mark'+classesStr).each(function() { 187 tan_activateKeyWord(tan_domToNum(this), f); 188 }); 189 190 return; 191 } 192 193 elem = $('#travel_editor mark.TANKeyWord').eq(foc); 194 195 if(elem.hasClass('active') === flag) 196 return; 197 198 if(flag === undefined) 199 elem.toggleClass('active'); 200 else 201 elem.toggleClass('active', flag); 202 203 elem.attr('data-adv', $('#navMenu_merchantSelect').find('option:selected').val()); 204 205 206 if(foc === currentFocus) $('#navMenu_buttonOnOff').toggleClass('active', elem.hasClass('active')); 207 208 // }}} 209 } 210 211 var totalReplaced = 0; 212 213 /** 214 * Surround the keyWords by <mark>, to highlighting it 215 * 216 * @param {RegExp} target regular expression matching the targeted keywords 217 */ 218 jQuery.fn.tan_highlightKeywords = function(target) 219 { 220 // {{{ 221 // 222 // Get all text nodes: 223 // 224 excludeNodes = ['SCRIPT', 'STYLE', 'H1', 'H2', 'H3', 'H4']; 225 var $textNodes = this 226 .find("*") 227 .andSelf() 228 .contents() 229 .filter(function() { 230 return this.nodeType === 3 231 && !$(this).parent("a").length 232 && !$(this).parent(excludeNodes.join(',')).length 233 && !$(this).parent('.TANKeyWord').length 234 && !$(this).parent('.TAN_link').length 235 && $(this).text().trim() !== ''; 236 }); 237 238 // 239 // Iterate through the text nodes, 240 // highlighting the keyWords 241 // 242 243 let totalReplaced = 0; 244 $textNodes.each(function(index, element) { 245 246 match = $(element).text().match(target); 247 248 if(match === null) return; 249 var i=0; 250 var finalStr = $(element).text().replace(target, function(word) { 251 var originalWord = match[i]; 252 i++; 253 totalReplaced++; 254 255 let tmp = addedKeyWords.filter(v=>v[0].format() === originalWord.format()) 256 257 let c = ''; 258 let s = originalWord; 259 if(tmp.length) 260 { 261 c = ' custom' 262 s = tmp[0][1]; 263 } 264 265 return `<mark class='TANKeyWord`+c+`' 266 data-search='`+s+`' 267 data-adv=`+$('#navMenu_merchantSelect').find('option:selected').val()+`>` 268 + originalWord 269 + `</mark>`; 270 }); 271 272 $(element).replaceWith(finalStr); 273 }); 274 275 $('.TANKeyWord').each(function(i) { 276 277 this.onclick = function() { 278 tan_changeFocus(tan_domToNum(this)); 279 }; 280 }); 281 282 // }}} 283 } 284 285 /** 286 * copy content from the travel ads network editor to the tiny MCE 287 */ 288 function copy_travel2wp() 289 { 290 // {{{ 291 292 $('#hidden_travel_editor').html(($('div#travel_editor').html())); 293 294 $('#hidden_travel_editor mark.TANKeyWord').each(function() { 295 $(this).replaceWith(this.outerHTML.replace(/\bmark\b/, 'span')) 296 }); 297 298 $('#hidden_travel_editor span.TANKeyWord').each(function() { 299 $(this).removeClass('current'); 300 $(this).removeClass('TANKeyWord'); 301 302 $(this).addClass('TAN_link'); 303 304 if($(this).hasClass('custom')) 305 $(this).addClass('TAN_link_custom'); 306 }); 307 308 $('#hidden_travel_editor section.tan_asset').each(function(i) { 309 $(this).find('script').siblings().remove(); 310 }); 311 312 setWpEditorContent( $('#hidden_travel_editor').html() ); 313 314 $('div#travel_editor section.tan_asset script').remove(); 315 316 $('#hidden_travel_editor').empty(); 317 318 // }}} 319 } 320 321 /** 322 * Copy the content from the wordpress editor to the travel editor 323 * 324 * @param {string} content The content to put in the travel editor 325 */ 326 function copy_wp2travel() 327 { 328 // {{{ 329 content = getWpEditorContent(); 330 331 332 if( $('div.mce-tinymce').css('display') === 'none' // normal mode 333 && $('wp-editor-area').css('display') === 'none') // text mode 334 { 335 $('#travel_editor').html('<h1 style="text-align:center">The visual Editor is not supported for now...</h1>'+ 336 '<h2 style="text-align:center">You can switch to autmatic mode!</h2>'); 337 $('#navMenu').toggle(false); 338 $('#navMenuOffset').toggle(false); 339 return; 340 } 341 $('#navMenu').toggle(true); 342 $('#navMenuOffset').toggle(true); 343 344 totalReplaced = 0; 345 $('#travel_editor').empty(); 346 $('#travel_editor').html(content); 347 $('#travel_editor span.TAN_link').each(function() { 348 349 $(this).replaceWith(this.outerHTML.replace(/\bspan\b/, 'mark')) 350 }); 351 352 $('#travel_editor mark.TAN_link').each(function() { 353 $(this).removeClass('TAN_link_custom'); 354 $(this).removeClass('TAN_link'); 355 356 $(this).addClass('TANKeyWord'); 357 }); 358 359 360 $("#travel_editor").tan_highlightKeywords(createNewRegexp(keyWordsRegExp, addedKeyWords.map(v=>v[0]))); 361 362 tan_changeFocus(currentFocus, false); 363 $('#navMenu_totalKeyWord').text($('#travel_editor mark.TANKeyWord').length); 364 // }}} 365 } 366 367 /** 368 * Transforms the google places API response to a easier to use object 369 * 370 * @param {Object} googlePlace The object returned by the google places API 371 * -> ``` 372 * { 373 * address_coponents: <array of addresses>, 374 * geometry: <geometry, lat, long>, 375 * types: <array of types>, 376 * name: <name> 377 * } 378 * ``` 379 * 380 * @return {Object} An easier to use array 381 * -> ``` 382 * { 383 * country: <country name>, 384 * city: <city name>, 385 * route: <route name>, 386 * searchText: <text to search on the OTA's calls>, 387 * lat: <latitude>, 388 * lng: <longitude>, 389 * } 390 * 391 * ``` 392 */ 393 function googlePlaceFormat(googlePlace) 394 { 395 // {{{ 396 let out = {}; 397 398 let cityWrongNames = { 399 'Krung Thep Maha Nakhon': 'Bangkok', 400 'Krong Siem Reap': 'Siem Reap' 401 }; 402 403 let address = googlePlace.address_components.filter(v=> !/^[0-9]*$/.test(v.long_name) // exclude only numbers 404 && !/^[0-9]/.test(v.long_name)); // and beginning with number 405 406 let country = address.filter(v=>v.types.contains('country')), 407 administrativeAreas = address.filter(v=>v.types.some(t=>/administrative_area_level_\d+/.test(t))), 408 city = address.filter(v=>v.types.contains('locality', 'postal_town')), 409 subLocalAreas = address.filter(v=>v.types.some(t=>/sublocality_level_\d+/.test(t))), 410 route = address.filter(v=>v.types.contains('route')); 411 412 out = { 413 'lat': googlePlace.geometry.location.lat(), 414 'lng': googlePlace.geometry.location.lng() 415 }; 416 417 if(country.length) 418 { 419 out.country=country[0].long_name; 420 if(city.length) 421 { 422 out.city=city[0].long_name; 423 424 if(route.length) 425 out.route = route[0].long_name 426 else if(subLocalAreas.length) 427 out.route = subLocalAreas[0].long_name 428 } 429 else if(administrativeAreas.length) 430 out.city = administrativeAreas[0].long_name; 431 } 432 433 out.city = cityWrongNames[out.city] || out.city; 434 435 436 if(!googlePlace.types.contains('lodging', 'premise')) // is not lodging nor premise 437 out.searchText = (out.route||'')+', '+(out.city||'')+', '+(out.country||''); 438 else 439 out.searchText = (googlePlace.name||'')+', '+(out.city||'')+', '+(out.country||''); 440 441 out.searchText = out.searchText.replace(/^[, ]+/, ''); 442 443 return out; 444 // }}} 445 } 446 447 /** 448 * Adds the custom keyWords to the original regexp 449 * 450 * @param {RegExp} original The original regexp conaining all the default keywords 451 * @param {Array} keyWords A list of the new keyWords 452 */ 453 function createNewRegexp(original, keyWords) 454 { 455 // {{{ 456 // 457 // transforming the old regexp to string 458 // 459 let newRegExp = original.toString().replace(/^[^\/]*\/|\/[^/]*$/g, ''); 460 461 // 462 // adding the new keyWords 463 // 464 for(let i=0 ; i<keyWords.length ; i++) 465 { 466 newRegExp += '|'; 467 if(/^[a-z]/i.test(keyWords[i])) // begin with latin alphabet 468 newRegExp += '\\b'; 469 470 newRegExp += keyWords[i].replace(/([\[\]\(\)\+\/\*\?])/g, '\\$1'); // making the word regex proof 471 472 if(/[a-z]$/i.test(keyWords[i])) // finish with latin alphabet 473 newRegExp += '\\b'; 474 475 newRegExp += '|'; 476 if(/^[a-z]/i.test(keyWords[i])) // begin with latin alphabet 477 newRegExp += '\\b'; 478 479 newRegExp += keyWords[i].format().replace(/([\[\]\(\)\+\/\*\?])/g, '\\$1'); // making the word regex proof 480 481 if(/[a-z]$/i.test(keyWords[i])) // finish with latin alphabet 482 newRegExp += '\\b'; 483 484 } 485 486 return new RegExp(newRegExp, 'gi'); 487 // }}} 488 } 489 490 /** 491 * initialize the google places field 492 */ 493 function initAutocomplete() 494 { 495 // {{{ 496 // 497 // avoids to trigger the page reload on enter 498 // 499 $('#googleField').on('keydown', function(e){if(e.keyCode===13) e.preventDefault()}); 500 501 googleField = new google.maps.places.Autocomplete($('#googleField')[0], 502 {types: ['establishment', 'geocode']}); 503 504 googleField.setFields(['address_components', 505 'geometry', 506 'types', 507 'name']); 508 509 googleField.addListener('place_changed', fillInAddress); 510 // }}} 511 } 512 513 /** 514 * Callback, listen to google places field 515 */ 516 function fillInAddress() 517 { 518 // {{{ 519 var place = googleField.getPlace(); 520 521 $('#googleField').addClass('kwAdd'); 522 let kw = $('#googleField').val().split(/, ?/)[0] 523 524 if(lastSel[0]) 525 { 526 let start = $(lastSel[1].startContainer).parents('mark.TANKeyWord'); 527 let end = $(lastSel[1].endContainer).parents('mark.TANKeyWord'); 528 529 lastSel[1].deleteContents(); 530 lastSel[1].insertNode(document.createTextNode(' '+kw+' ')); 531 532 start.replaceWith(function() { return this.innerHTML }); 533 end.replaceWith(function() { return this.innerHTML }); 534 } 535 536 if( !addedKeyWords.map(v=>v[0].format()).contains(kw.format()) 537 && ( !kw.match(keyWordsRegExp) 538 || kw.match(keyWordsRegExp)[0] !== kw )) // kw not present in predefined keywords 539 addedKeyWords.push([kw, googlePlaceFormat(place).searchText]); 540 541 if(addedKeyWords.length) 542 $('#newKWs').html('<br/>Your keywords:<br/> '+addedKeyWords.map(v=>'<span class=newKW>'+v[0]+'</span>').join(' ')); 543 544 545 $('span.newKW').click(function() { 546 let kw = this.innerHTML; 547 548 addedKeyWords = addedKeyWords.filter(v=>v[0] !== kw); 549 $('.TANKeyWord').each(function() { 550 if(kw.format() === this.innerText.format()) 551 $(this).replaceWith(this.innerHTML); 552 }); 553 $(this).remove(); 554 }); 555 556 557 $("#travel_editor").tan_highlightKeywords(createNewRegexp(keyWordsRegExp, addedKeyWords.map(v=>v[0]))); 558 // }}} 559 } 560 561 // 562 // setting up the listeners 563 // 564 565 $('div#navMenu_buttonOnOff').click(function(){ 566 tan_activateKeyWord(); 567 }); 568 569 $('div[aria-label="TravelAdsNetwork Manual"]').click(function() { 570 // {{{ 571 if($('#travelads_sectionid').css('display') === 'none') 572 { 573 setTimeout(function() { 574 $('html, body').animate({ 575 scrollTop: $('#travelads_sectionid').offset().top - 100 576 }, 500); 577 }, 300); 578 } 579 // }}} 580 }); 581 582 $('#navMenu_merchantSelect').change(function() { 583 $('.TANKeyWord.current').attr('data-adv', $(this).find('option:selected').val()) 584 tan_activateKeyWord(undefined, true); 585 }); 586 587 $('#navMenuprevWord').click(function() { 588 tan_changeFocus(currentFocus-1); 589 }); 590 $('#navMenunextWord').click(function() { 591 tan_changeFocus(currentFocus+1); 592 }); 593 594 $('#publish').click(function() { 595 copy_travel2wp(); 596 }); 597 598 599 let stop=setInterval(function() { 600 // {{{ 601 if(!(window.tinyMCE || window.tinymce).editors) 602 return; 603 604 clearInterval(stop); 605 606 $('#travelads_sectionid').on('mouseenter', function() { 607 if(!travelFocused) 608 { 609 travelFocused = true; 610 copy_wp2travel(); 611 } 612 613 let editors = ['#content', '.editor-writing-flow'] 614 .concat([...(window.tinyMCE || window.tinymce).editors] 615 .map(v=>(!!v.container && '#'+v.container.getAttribute('id') || ''))) 616 .filter(v=>!!v) 617 .join(', '); 618 619 $(editors).each(function() { 620 this.onmouseenter = function() { 621 if(travelFocused) 622 copy_travel2wp(); 623 travelFocused = false; 624 }; 625 }); 626 }); 627 628 let content = getWpEditorContent(); 629 630 var parser = new DOMParser(); 631 data = parser.parseFromString('<html>'+content.replace(/ /g, '<br/>')+'</html>', "text/xml"); 632 633 [...data.getElementsByClassName('TAN_link_custom')].forEach(function(elem) { 634 let flag=-2; 635 addedKeyWords.forEach(function([kw], index) { 636 if(kw.format() === elem.innerHTML.format()) 637 if(kw.format() !== kw) 638 flag = index; 639 else 640 flag = -1; 641 }); 642 643 switch(flag) 644 { 645 case -2: 646 addedKeyWords.push([elem.innerHTML, elem.getAttribute('data-search')]); 647 break; 648 case -1: 649 break; 650 default: 651 addedKeyWords[flag] = [elem.innerHTML, elem.getAttribute('data-search')]; 652 } 653 }); 654 655 if(addedKeyWords.length) 656 $('#newKWs').html('<br/>Your keywords:<br/> '+addedKeyWords.map(v=>'<span class=newKW>'+v[0]+'</span>').join(', ')); 657 // }}} 658 }, 500); 659 660 661 let ctrlDown = false; 662 let shiftDown = false; 663 let lastSel = []; 664 665 /** 666 * Fast mode, allow to go through the keywords 667 * with the keyboard, much faster than the mouse 668 */ 669 $('#navMenu_keyWord').on('keydown', function(e) { 670 // {{{ 671 let code = e.keyCode; 672 let sel = $("#navMenu_merchantSelect"); 673 switch(code) 674 { 675 case 39: // right 676 tan_changeFocus(currentFocus+1); 677 break; 678 case 37: 679 tan_changeFocus(currentFocus-1); 680 break; // left 681 case 38: // up 682 sel.val((sel.find(':selected').prev('option')[0] || sel.find('option:last-child')[0]).value); 683 sel.trigger('change'); 684 break; 685 case 40: // down 686 sel.val((sel.find(':selected + option')[0] || sel.find('option:first-child')[0]).value); 687 sel.trigger('change'); 688 break; 689 case 32: tan_activateKeyWord(ctrlDown?-1:shiftDown?-2:currentFocus); break; // space 690 case 13: tan_activateKeyWord(ctrlDown?-1:shiftDown?-2:currentFocus); break; // enter 691 692 case 17: ctrlDown=true; break; 693 case 16: shiftDown=true; break; 694 } 695 696 e.preventDefault(); 697 // }}} 698 }) 699 $('#navMenu_keyWord').on('keyup', function(e) { 700 // {{{ 701 if(e.keyCode === 17) 702 ctrlDown=false; 703 if(e.keyCode === 16) 704 shiftDown=false; 705 // }}} 706 }); 707 708 $('#travel_editor, #navMenu tr:last-child').on('mouseup', function(e) { 709 // {{{ 710 setTimeout(function() { 711 if( !$(e.target).is("#navMenu_merchantSelect") 712 && !getSelectionHtml()[0] 713 && !$('#googleField').is(':focus')) 714 $('#navMenu_keyWord')[0].focus(); 715 }, 100); 716 // }}} 717 }) 718 719 $('#travel_editor').on('mouseup', function(e) { 720 // {{{ 721 let sel = getSelectionHtml(); 722 if(!sel[0]) 723 return; 724 725 lastSel = sel; 726 $(lastSel[1]).parents('mark.TANKeyWord').replaceWith(function() { 727 return this.innerHTML; 728 }) 729 sel = sel[0]; 730 731 $('#googleField').val(sel.replace(/<[^>]*>/g, '')); 732 $('#googleField')[0].focus(); 733 // }}} 734 }); 735 736 document.addEventListener('scroll', function (e) { 737 // {{{ 738 739 let topOffset = $('#wpadminbar').height() + 740 (window.editorName==='gutenberg'?$('.edit-post-header').outerHeight():0) + 741 ((tmp=$('.components-notice')).length?[...tmp].reduce((a,v)=>a+$(v).outerHeight(),0):0), 742 diff = $(window).scrollTop() - $('#travelads_sectionid .inside').offset().top + topOffset; 743 744 if(diff >= 0) 745 { 746 $('#navMenu').toggleClass('flying', true); 747 748 $('#navMenu').css({ 749 position: 'fixed', 750 left: $('#travelads_sectionid').offset().left, 751 top: topOffset, 752 753 width: $('#travelads_sectionid').width()+2, 754 }); 755 756 $('.pac-container.pac-logo').css({ 757 top: $('#googleField').offset().top + $('#googleField').height() + 8, 758 }); 759 760 } 761 else 762 { 763 $('#navMenu').toggleClass('flying', false); 764 $('#navMenu').css({ 765 position: '', 766 left: '', 767 top: '', 768 width: '' 769 }); 770 } 771 // }}} 772 }, true); 773 774 initAutocomplete(); 775 776 777 // 778 // creation of the Regex (list of keywords) and initializations 779 // 780 data=keyWords; 781 var to_replace = ''; 782 for(var i=0 ; i<data.length ; i++) 783 to_replace += '\\b'+data[i]+'\\b|'; 784 785 to_replace = to_replace.slice(0, -1); 786 787 keyWordsRegExp = new RegExp(to_replace, 'gi') 788 789 790 // 791 // Open the dialog by default 792 // 793 setTimeout(function() { 794 $('#travelads_sectionid.closed button').click(); 795 }, 2000); 796 797 // }}} 798 }); 785 799 }(jQuery); -
traveladsnetwork-com/trunk/readme.txt
r1969530 r2083624 2 2 Contributors: christophesecher 3 3 Tags: travel, affiliate plugin, affiliate link, deeplinking, deeplinks, travel booking, bloggers, merchant, travel content, travel, Travel Ads Network, Traveladsnetwork, content monetization, creating purchase intent, monetize your audience data, Convert Content into Revenue 4 Tags: travel affiliate plugin affiliate link deeplinking deeplinks travel booking bloggers merchant travel content travel Travel Ads Network Traveladsnetwork content monetization creating purchase intent monetize your audience data Convert Content into Revenue5 4 Requires at least: 4.9.0 6 Tested up to: 4.9.85 Tested up to: 5.2 7 6 Requires PHP: 7.3 8 7 Stable tag: trunk
Note: See TracChangeset
for help on using the changeset viewer.