(function() { angular.module('apesterWpApp', [ 'apeTemplates', 'ui.select', 'ape.background', ]) // config interaction box bg image .config(['abg_configProvider', function (abg_configProvider) { abg_configProvider.setImgixMediaFilterUrl('https://images.apester.com/'); }]) .filter('filterInteractionTitle', function() { return function (items, searchTerm) { var filtered = []; var SearchRegex = new RegExp(searchTerm, 'i'); if (items) { for (var i = 0; i < items.length; i++) { var item = items[i]; // search interaction titles without their wrapping html tags if (SearchRegex.test(item.title.replace(/<(?:.|\n)*?>/gm, ''))) { filtered.push(item); } } } return filtered; }; }) .filter('apePlainText', function() { return function(html) { html = html || ''; return (html || '').replace(/(<([^>]+)>)/ig,' ').replace(/\s\s+/g, ' ').trim(); }; }) .filter('apeEmphasis', function() { return function(val, maxLength) { if (val && val.length > maxLength) { val = val.substr(0, maxLength) + '...'; } return val; }; }); })(); (function() { angular.module('apesterWpApp') .constant('configuration', { displayBaseUrl: 'https://display.apester.com/', tokenBaseUrl: 'https://users.apester.com/publisher/token/', interactionBaseUrl: 'https://interaction.apester.com', interactionsByTokenUrl: '/interaction/all/token?', eventsBaseUrl: 'https://events.apester.com/event', }); })(); (function() { angular.module('apesterWpApp') .directive('dashboard', ['InteractionStatsService', 'InteractionService', 'TokensService', 'EventsService', '$window', function(InteractionStatsService, InteractionService, TokensService, EventsService, $window){ return { restrict: 'E', templateUrl: 'dashboard.html', link: function(scope, element, attrs) { scope.pluginPath = $window.apester_plugin_path; scope.interactionFilter = {}; scope.interactionSearchFilter = ''; scope.interactionSortMethod = 'created'; scope.isSortByDisabled = true; // set sort by ctr/date to disable until we get the data from server scope.interactionsCount = 0; scope.interactionsState = 'published'; // expose sendEvent method to dashboard scope so all child scopes can use it, while only injecting EventsService once here scope.sendEvent = EventsService.sendEvent; var addStatsToInteractions = function(statisticsArray) { for (var i = 0; i < scope.interactions.length; i++) { var interaction = scope.interactions[i]; interaction.ctr = statisticsArray[interaction.interactionId].ctr; interaction.views = statisticsArray[interaction.interactionId].views; } }; function fetchInteractions(tokens) { InteractionService.getInteractions(tokens) .then(function(interactions) { scope.interactions = interactions; scope.interactionsCount = InteractionStatsService.beautifyNumber(interactions.length); fetchStats(scope.interactions); mapEngineIntoInteractions(scope.interactions); }, function(error) { scope.isError = true; console.error('Error getting interaction: ', error); }) } function fetchStats(interactions) { InteractionStatsService.fetchStatistics(interactions, {pretty: true}).then(function(stats) { scope.statistics = stats; addStatsToInteractions(stats); scope.isSortByDisabled = false; }); } var mapIconClassByEngine = function(engine) { var map = { 'multi-poll': 'icon-poll', 'multi-trivia': 'icon-quiz', 'personality-quiz': 'icon-personality', 'video-trivia': 'icon-video', 'video-poll': 'icon-video', 'video-personality': 'icon-video', 'contest-poll': 'icon-countdown', 'journey': 'icon-rolling-story' }; return map[engine]; }; /** * Adds 'engine' property & respectful 'iconClass' property to each interaction based on it's layoutId property * @param interactions [Array] - the interactions that are missing the engine property (e.g. interactions from server) */ var mapEngineIntoInteractions = function(interactions) { InteractionService.getEngines().then(function(engines){ // go over interactions for ( var i = 0; i < interactions.length; i++) { interactionLayoutId = interactions[i].layout; // go over engines for ( var j = 0; j < engines.length; j++) { if (interactionLayoutId === engines[j].layoutId) { var engineDirective = engines[j].directive; interactions[i].engineFullName = engineDirective; if (engineDirective.indexOf('-two') !== -1) { interactions[i].engine = engineDirective.substring(0, engineDirective.length - 4); } else { interactions[i].engine = engineDirective; } // add the icon class property to each interaction interactions[i].iconClass = mapIconClassByEngine(interactions[i].engine); } } } }); }; function init() { // get the apester channels tokens from Wordpress db scope.apesterTokens = TokensService.getPureTokens() || []; if (scope.apesterTokens.length === 0) { scope.isNoToken = true; } else { // init the tokens service - get channel data based on token TokensService.init(scope.apesterTokens); fetchInteractions(scope.apesterTokens); } } init(); } } }]); })(); (function($) { angular.module('apesterWpApp') .service('EventsService', ['TokensService', '$http', '$window', 'configuration', function(TokensService, $http, $window, configuration){ var eventBaseProps = { event: '', properties: { pluginProvider: 'WordPress', pluginVersion: $window.apester_plugin_version, channelToken: TokensService.getPureTokens()[0] || '', phpVersion: window.php_version, wpVersion: window.wp_version }, metadata: { referrer: encodeURIComponent(document.referrer), current_url: encodeURIComponent($window.location.href), screen_height: $window.screen.height.toString(), screen_width: $window.screen.width.toString(), language: $window.navigator.userLanguage || $window.navigator.language } }; var sendEvent = function(eventDataObject) { // if no event name provided - stop execution if (!eventDataObject.event) { console.error('Event not sent - missing event name'); return; } var eventData = angular.merge({}, eventBaseProps, eventDataObject); // make sure we remove all empty properties for (var prop in eventData.properties) { if (eventData.properties.hasOwnProperty(prop)) { if (eventData.properties[prop].trim() === '') { delete eventData.properties[prop]; } } } $http({ url: configuration.eventsBaseUrl, method: 'POST', contentType: 'application/json; charset=UTF-8', data: JSON.stringify(eventData), async: false }); }; return { sendEvent: sendEvent } }]); })(jQuery); (function() { angular.module('apesterWpApp') .service('InteractionService', ['$q', '$http', 'configuration', function($q, $http, configuration){ var limit = '&limit=50'; var dateFilterSuffix = '&fromDate='; var convertTokensToUrlParams = function(tokens) { var url = ''; for (var i = 0; i < tokens.length; i++) { url += 'tokenId=' + tokens[i]; if (i !== tokens.length - 1) { url +='&'; } } return url; }; var engines = []; /** * Return the payload from successful response * @param {object} response * @returns {*} */ function returnPayload(response) { return response.data.payload; } var getEngines = function() { var result; /** * gets base engine layout */ var getBaseEngine = function() { return $http({ url: configuration.interactionBaseUrl + '/engine', method: 'GET', withCredentials: true }).then(returnPayload); }; /** * gets all engines and caches them into the 'engines' propery */ var getAllEngines = function() { return getBaseEngine().then(function(engines){ var baseEngineId = engines[0].engineId; return $http({ url: configuration.interactionBaseUrl + '/engine/' + baseEngineId + '/layout', method: 'GET' }).then(function(response) { // cache the result before we return engines = response.data.payload; return engines; }); }) }; if (engines.length === 0) { result = getAllEngines(); } else { result = $q.defer().resolve(engines); } return result; }; var getInteractions = function(tokens) { // get interactions from 30 days back var nowDate = new Date(); nowDate.setMonth(nowDate.getMonth() - 1); var dateString = encodeURIComponent(nowDate.toDateString()); return $http({ url: configuration.interactionBaseUrl + configuration.interactionsByTokenUrl + convertTokensToUrlParams(tokens) + limit + dateFilterSuffix + dateString, method: 'GET', withCredentials: true }).then(returnPayload); }; return { getEngines: getEngines, getInteractions: getInteractions } }]); })(); (function($) { angular.module('apesterWpApp') .service('RandomService', ['$q', '$http', 'TokensService', 'configuration', function($q, $http, TokensService, configuration){ /** * Return the payload from successful response * @param {object} response * @returns {*} */ function returnPayload(response) { return response.data.payload; } var getSingleChannelPlaylist = function(tokenFullObject) { return $http({ url: configuration.displayBaseUrl + 'tokens/' + tokenFullObject.token + '/interactions/random', method: 'GET', cache: true }).then(function(response) { tokenFullObject.random = returnPayload(response); return tokenFullObject; }, function() { return; }); }; var getTokensPlaylistData = function(tokensFullDataList) { var promisesArr = []; for (var i=0 ;i < tokensFullDataList.length; i++) { promisesArr.push(getSingleChannelPlaylist(tokensFullDataList[i])); } return $q.all(promisesArr).then(function(response){ // filter only tokens that returned with random interactions in playlist return response.filter(function(item){return item;}) }, function(e) { console.error(e); }); }; return { getTokensPlaylistData: getTokensPlaylistData } }]); })(jQuery); (function($) { angular.module('apesterWpApp') .service('TokensService', ['$q', '$http', '$window', 'configuration', 'Utils', function($q, $http, $window, configuration, Utils){ // make sure the service data can only be initialized once var isInitialized = false; var apesterTokens = []; var tokensFullData = []; // get the apester channels tokens from Wordpress db (after we put them into global window variable 'apester_tokens' in 'tinymce.php' file var pureTokens = Object.keys($window.apester_tokens) || []; // make sure we convert a value that is not an array (e.g. just one token string from older version of the plugin) // into an array with that one value it contains pureTokens = $.isArray(pureTokens) ? pureTokens : [pureTokens]; /** * Return the payload from successful response * @param {object} response * @returns {*} */ function returnPayload(response) { return response.data.payload; } var getSingleTokenData = function(token) { return $http({ url: configuration.tokenBaseUrl + token, method: 'GET', cache: true, withCredentials: true }).then(function(response) { var data = returnPayload(response); return !!data && { token: data.authToken, name: data.name }; }); }; var getTokensFullData = function() { var promisesArr = []; for (var i=0 ;i < apesterTokens.length; i++) { promisesArr.push(getSingleTokenData(apesterTokens[i])); } return $q.all(promisesArr).then(function(responses){ // filter only tokens that returned valid result return responses.filter(function(item){return item;}) }, function(e) { console.error(e); }); }; /** * returns a list of the channel tokens as they are being saved on the server side * @returns {*|Array} */ var getPureTokens = function() { return Utils.getValidTokens(pureTokens); }; var init = function(tokens) { apesterTokens = Utils.getValidTokens(tokens); if (!isInitialized) { getTokensFullData(); isInitialized = true; } }; return { init: init, getTokensFullData: getTokensFullData, getPureTokens: getPureTokens } }]); })(jQuery); (function() { angular.module('apesterWpApp') .service('Utils', ['$filter', function($filter) { /** * validate token on the client * @param token * @returns {boolean} isValid */ var isValidToken = function(token) { return /^[0-9a-fA-F]{24}$/.test(token); }; /** * filters valid tokens from a tokens array * @param tokens - array of {string} tokens * @returns {array} - valid tokens array */ var getValidTokens = function (tokens) { return $filter('filter')(tokens, function(token){ return isValidToken(token); }); }; return { isValidToken: isValidToken, getValidTokens: getValidTokens } }]); })(); (function() { angular.module('apesterWpApp') .directive('interactionBox', ['$filter', '$sce', '$timeout', function($filter, $sce, $timeout){ return { restrict: 'E', replace: true, templateUrl: 'interactionBox.html', link: function(scope, element, attrs) { var JOURNEY_LAYOUT_ID = '574d5ff5bdeb9c513bdd738e'; var isJourney = scope.interaction.layout === JOURNEY_LAYOUT_ID; scope.boxTitle = $sce.trustAsHtml($filter('apeEmphasis')($filter('apePlainText')(scope.interaction.title, ' '), 50)); scope.playerPreviewUrl = 'https://renderer.apester.com/interaction/' + scope.interaction.interactionId + '?iframe_preview=true'; scope.duplicatInteractionUrl = 'https://app.apester.com/editor/new?duplicate=' + scope.interaction.interactionId + '&isJourney=' + isJourney; // watch the modal open state - when opened, make sure to update the background image so the ape.background // library will update according to the view, otherwise it won't know the dimensions since modal is closed scope.$watch('isOpen', function(oldVal, newVal) { if (scope.isOpen) { $timeout(function () { scope.background = scope.interaction.data.backgroundImage || scope.interaction.image; }, 0); } }); } } }]); })(); (function() { angular.module('apesterWpApp') .directive('interactionStats', ['$filter', function($filter){ return { restrict: 'E', replace: true, templateUrl: 'interactionStats.html', link: function(scope, element, attrs) { } } }]); })(); (function() { angular.module('apesterWpApp') .service('InteractionStatsService', ['$q', '$http', '$window', function($q, $http, $window){ /** * BI base URL * @type {string} */ var baseUrl = ($window.location.protocol) + '//gcp-analytics.apester.com/api/interaction/'; var factors = [ 'views', 'ctr', 'circulation', 'social', 'votingStage' ]; var interactionsStatistics = {}; var buildConfigObject = function(interactions){ function extractMinDate(interactions){ var unixDateArray = interactions.map(function(interaction){ return new Date(interaction.created).getTime(); }); var min; try { min = unixDateArray.reduce(function(minimal, current){ if(minimal > current){ return current ; } return minimal; }); } catch ( e ) { min = new Date('2016-06-12').getTime(); } return min; } var ret = {}; ret.metrics = ['clicked_social', 'clicked_other_interaction','interaction_loaded', 'interaction_started', 'interaction_vote_stage']; ret.interactionIds = interactions.map(function(interaction){ return interaction.interactionId;}); ret.to = (new Date()).getTime(); ret.from = extractMinDate(interactions); return ret; }; var getStatisticsData = function(configObject) { var query = '?' + 'from=' + configObject.from + '&to=' + configObject.to + '&ids=' + configObject.interactionIds; return $http.get(baseUrl + configObject.metrics.join(',') + query).then(function(res){ return res; }); }; /** * Creates an interaction cell within the stats object. * @param interactionId */ function initiateInteractionStat(interactionId) { if (!interactionsStatistics[interactionId]) { interactionsStatistics[interactionId] = {}; } } /** * Gather data from many months and put it into one number. * @param data * @param property */ function normalizeStats(data, property) { Object.keys(data).forEach(function(key) { var interaction = data[key]; var total = 0; Object.keys(interaction).forEach(function(month) { total += interaction[month]; }); initiateInteractionStat(key); interactionsStatistics[key][property] = total; }); } /** * Adds zeros where fields are empty. */ function padWithZeros(interactions, interactionsStatistics) { interactions.forEach(function(interaction) { var id = interaction.interactionId; initiateInteractionStat(id); factors.forEach(function(factor) { if (!interactionsStatistics[id][factor]) { interactionsStatistics[id][factor] = 0; } }); }); } /** * Beautifies a single number. * @param value * @returns {*} */ function beautifyNumber(value) { var powers = [ {key: 'q', value: Math.pow(10,15)}, {key: 't', value: Math.pow(10,12)}, {key: 'b', value: Math.pow(10,9)}, {key: 'm', value: Math.pow(10,6)}, {key: 'k', value: Math.pow(10,3)} ]; // Return the value if it's below our lowest power. if (value < powers[powers.length - 1].value) { return value; } // Find the maximal power that our value is bigger than. var num; for(var i = 0; i < powers.length; i++) { if(value >= powers[i].value) { num = value / powers[i].value; break; } } // Avoid a decimal point when it's 0. if (num % 1 < 0.1) { return num.toFixed() + powers[i].key; } // Show only one decimal point. return num.toFixed(1) + powers[i].key; } /** * Optionally beautify results. */ function beautify(interactions, interactionsStatistics) { /** * Takes two numbers and makes a percentage form them. * @param num * @param total */ function toPercent(num, total) { if (num === 0 && total === 0) { return 0; } var percent = num / total * 100; if (percent > 100) { percent = 100; } return Math.ceil(percent); } // For each cell in the stats, beautify it. interactions.forEach(function(interaction) { var id = interaction.interactionId; var data = interactionsStatistics[id]; // patch to calc countdown CTR if (data.votingStage) { data.ctr = toPercent(data.ctr, data.votingStage); } else { data.ctr = toPercent(data.ctr, data.views); } // For each key of the cell. factors.forEach(function(factor) { if (factor === 'ctr') { return; } data[factor + 'Readable'] = beautifyNumber(data[factor]); }); }); } var changeDataFormatFromApesterToMixpanel = function(data, existObject , eventName) { data = data.data.payload.data; Object.keys(data).forEach( function(interactionId) { existObject[interactionId] = {'2015-12-01': data[interactionId][eventName]}; }); }; return { fetchStatistics: function(interactions, options) { var deferred = $q.defer(), promises = []; // Filter interactions that we already have data on. interactions = interactions.filter(function(interaction) { return !interactionsStatistics[interaction.interactionId]; }); // If we already have all interactions cached, just return the stats. if (interactions.length === 0) { deferred.resolve(interactionsStatistics); return deferred.promise; } var apesterDataConfig = buildConfigObject(interactions); getStatisticsData(apesterDataConfig).then(function(results){ var newResults = [{},{},{},{}, {}]; changeDataFormatFromApesterToMixpanel(results, newResults[0],'interaction_loaded'); changeDataFormatFromApesterToMixpanel(results, newResults[1],'interaction_started'); changeDataFormatFromApesterToMixpanel(results, newResults[2],'clicked_other_interaction'); changeDataFormatFromApesterToMixpanel(results, newResults[3],'clicked_social'); changeDataFormatFromApesterToMixpanel(results, newResults[4],'interaction_vote_stage'); results = newResults; // Compute the raw data from many months to one total number. factors.forEach(function(factor, i) { normalizeStats(results[i], factor); }); // Add zeros where we didn't get data. padWithZeros(interactions, interactionsStatistics); beautify(interactions, interactionsStatistics); deferred.resolve(interactionsStatistics); }, function(err) { deferred.reject(err); }); return deferred.promise; }, beautifyNumber: beautifyNumber } }]); })(); (function() { angular.module('apesterWpApp') .directive('apeActionItems', [function(){ return { restrict: 'E', replace: true, templateUrl: 'action-items.html', link: function(scope, element, attrs) { scope.embedInteraction = function(interaction) { scope.insertNormalShortCode(interaction.interactionId); scope.sendEvent({ event: 'wordpress_embed_interaction_clicked', properties: { interactionId: interaction.interactionId, channelId: interaction.publisherId, channelToken: ''// send event, but with empty channelToken so that prop gets removed from the final event (it's irrelevant) } }); }; } } }]); })(); (function() { angular.module('apesterWpApp') .directive('apeHeader', [function(){ return { restrict: 'E', replace: true, templateUrl: 'header.html', link: function( scope, element, attrs ) { } } }]); })(); (function() { angular.module('apesterWpApp') .directive('searchBox', [function(){ return { restrict: 'E', replace: true, templateUrl: 'search.html', link: function(scope, element, attrs) { var elem = element.find('input'); scope.filterByTitle = function(stringToFind) { scope.interactionFilter.title }; scope.focusOrsearchInteractions = function($event) { $event.preventDefault(); if (!scope.focus) { elem[0].focus(); } else { scope.searchInteractions(); } } } } }]); })(); (function() { angular.module('apesterWpApp') .directive('apeNavbar', ['TokensService', 'RandomService', function(TokensService, RandomService){ return { restrict: 'E', replace: true, templateUrl: 'ape-navbar.html', link: function(scope, element, attrs) { scope.tokensWithPlaylist = []; // get channel names for each token TokensService.getTokensFullData().then(function(response){ // filter only tokens that have items in playlist RandomService.getTokensPlaylistData(response).then(function(randomResponse) { scope.tokensWithPlaylist = randomResponse; }); }); } } }]); })(); (function() { angular.module('apesterWpApp') .directive('apeDataFilter', [function(){ return { restrict: 'E', replace: true, templateUrl: 'dataFilters.html', link: function(scope, element, attrs) { var filters = {}; var SORT_BY = "Sort By "; /** * @desc * calls when scroll reach the bottom of publisher list */ scope.fetchPublishers = function() { }; scope.interactionFilters = [ { 'name': 'All Engines', 'layout': '', 'classes': 'all-engines' }, { 'name': 'Poll', 'classes': 'engine-filter ic icon-poll', 'layout': 'multi-poll' }, { 'name': 'Quiz', 'classes': 'engine-filter ic icon-quiz', 'layout': 'multi-trivia' }, { 'name': 'Personality', 'classes': 'engine-filter ic icon-personality', 'layout': 'personality-quiz' }, { 'name': 'Video', 'classes': 'engine-filter ic icon-video', 'layout': 'video-poll' }, { 'name': 'Countdown', 'classes': 'engine-filter ic icon-countdown', 'layout': 'contest-poll' }, { 'name':'Journey', 'classes':'engine-filter ic icon-rolling-story', 'layout': 'journey' } ]; scope.changeEngineFilter = function(engine) { scope.interactionFilter.engine = engine; }; scope.changeSortFilter = function(sortMethod) { scope.interactionSortMethod = sortMethod; }; function init() { scope.fetchPublishers(); scope.interactionsState = 'Published'; scope.interactionsStates = ['Published', 'Saved', 'Archived']; scope.sortOptions = [ { label: SORT_BY + 'CTR', sortMethod: 'ctr' }, { label: SORT_BY + 'Date', sortMethod: 'created' }, { label: SORT_BY + 'Views', sortMethod: 'views' }]; scope.sortOption = scope.sortOptions[1]; scope.interactionSortMethod = scope.sortOption.sortMethod; scope.publishers = []; scope.interactionsLayout = scope.interactionFilters[0]; } init(); } } }]); })(); (function() { angular.module('apesterWpApp') .directive('channelSelector', ['TokensService', 'RandomService', '$http', function(TokensService, RandomService, $http){ return { restrict: 'E', replace: true, templateUrl: 'channel-selector.html', link: function(scope, element, attrs) { scope.placeholder = 'Embed manual playlist'; scope.embedRandomMedia = function() { scope.insertRandomShortCode(scope.tokensWithPlaylist[0].token); }; scope.embedSelectedRandomMedia = function(selectedToken, $select) { // after resetting the $select.selected the change event will call this function again, // so we check if we received 'selectedToken' if (selectedToken) { scope.insertRandomShortCode(selectedToken); // reset selected option so we keep persenting the placeholder after any change delete $select.selected; } }; } } }]); })(); (function() { angular.module('apesterWpApp') .directive('apePlaylistStatus', [function(){ return { restrict: 'E', replace: true, templateUrl: 'playlist-status.html', link: function(scope, element, attrs) { scope.isIncludePlaylist = true; var isTinyMceEditorExists = function () { return !!tinymce && !!tinymce.activeEditor; }; var getArticleContent = function () { return tinymce.activeEditor.getContent(); }; scope.$watch("isOpen", function (newIsOpen) { var articleContent; if (!newIsOpen) { return; } if (isTinyMceEditorExists()) { articleContent = getArticleContent(); scope.isIncludePlaylist = articleContent.indexOf('[apester-exclude-playlist]') === -1; } }); scope.togglePlaylistStatus = function () { scope.isIncludePlaylist = !scope.isIncludePlaylist; if (!scope.isIncludePlaylist) { insertExcludePlaylistShortCode(); } else { removeExpludePlaylistShorcodeFromEditor(); } }; var insertExcludePlaylistShortCode = function () { if (isTinyMceEditorExists()) { tinymce.activeEditor.insertContent('[apester-exclude-playlist]'); } }; var removeExpludePlaylistShorcodeFromEditor = function () { var articleContent; if (isTinyMceEditorExists()) { articleContent = getArticleContent(); tinymce.activeEditor.setContent(articleContent.replace(/\[apester-exclude-playlist\]/g, '')); } }; } } }]); })(); (function(jQuery) { var ngApp; angular.module('apesterWpApp') .directive('apesterModal', ['EventsService', function(EventsService) { return { restrict: 'E', templateUrl: 'apester-modal.html', link: function(scope, element, attrs) { var wpEditor; scope.isOpen = false; scope.insertNormalShortCode = function(interactionId) { if (tinymce && tinymce.activeEditor) { tinymce.activeEditor.insertContent('[interaction id="' + interactionId + '"]'); } scope.isOpen = false; }; scope.insertResultsShortCode = function(interactionId) { if (tinymce && tinymce.activeEditor) { tinymce.activeEditor.insertContent('[interaction id="' + interactionId + '" results="true"]'); } scope.isOpen = false; }; scope.insertRandomShortCode = function(channelToken) { if (tinymce && tinymce.activeEditor) { tinymce.activeEditor.insertContent('[apester-playlist channelToken="' + channelToken + '"]'); EventsService.sendEvent({ event: 'wordpress_use_playlist', properties: { channelToken: channelToken } }); } scope.isOpen = false; }; scope.closeModal = function() { scope.isOpen = false; }; tinymce.PluginManager.add('apester_btn', function (editor, url) { var ngApp; wpEditor = editor; var pluginPath = url.substr(0, url.lastIndexOf('/')); // Add Button to TinyMCE Visual Editor Toolbar editor.addButton('apester_btn', { title: 'Apester', cmd: 'apesterLogic', icon: 'apester-btn', // apply an icon class (only applies if no image is passed) image: pluginPath + '/img/ape-icon.svg' }); editor.addCommand('apesterLogic', function() { scope.isOpen = true; EventsService.sendEvent({ event: 'wordpress_apester_button_clicked' }); scope.$apply(); // must be present since the current callback is being called outside of angular's context }); }); } } }]); // insert angular app element for manual bootstrap jQuery('body').append('
'); angular.element(function() { return (function(){ ngApp = angular.bootstrap(document.getElementById('apesterWpApp'), ['apesterWpApp']); })() }); })(jQuery);