WME PL Jump

Opens a PL in an existing WME window/tab.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

// ==UserScript==
// @name            WME PL Jump
// @description     Opens a PL in an existing WME window/tab.
// @version         2020.11.23.01
// @author          The_Cre8r and SAR85
// @copyright       The_Cre8r and SAR85
// @license         CC BY-NC-ND
// @grant           none
// @include         /^https:\/\/(www|beta)\.waze\.com\/(?!user\/)(.{2,6}\/)?editor\/?.*$/
// @namespace       https://github.com/TheCre8r/WME-PL-Jump-Release/
// @require			https://greasyfork.org/scripts/24851-wazewrap/code/WazeWrap.js
// ==/UserScript==

/* global OpenLayers */
/* global W */
/* global $ */
/* global WazeWrap */


(function () {
    'use strict';
    var app,
        AppView,
        jumpInput,
        JumpView,
        LinkView,
        Link,
        links,
        LinkCollection,
        tab,
        c1,
        c2,
        c3;
    function log(txt) {
        if ('object' === typeof txt) {
            //text = JSON.stringify(text);
        }
        console.log('PLJ: ' + txt);
    }

    /**
     * Checks for necessary page elements or objects before initializing
     * script.
     * @param tries {Number} The number of tries bootstrapping has been
     * attempted.
     */
    function bootstrap(tries) {
        tries = tries || 0;
        if (WazeWrap.Ready && $ &&
            window.Backbone && $('#edit-buttons').length) {
            init();
        } else if (tries < 20) {
            window.setTimeout(function () {
                bootstrap(tries + 1);
            }, 1000);
        }
    }

    /**
     * Initializes global variables and sets up HTML and event listeners.
     */
    function init() {
        var tabContent = '<div id="pljumptabcontent"></div>';

        if (!$('#pljumpinput').length) {
            initializeLink();
            initializeCollection();
            links = new LinkCollection;
            tab = new WazeWrap.Interface.Tab('PLJump', tabContent, init2);
            updateAlert();
            app = new AppView({ collection: links });
        }

    }
    function init2() {
        if (!$('#pljumpinput').length) {
            initializeViews();
            jumpInput = new JumpView({ collection: links });
            W.editingMediator.on('change:editingHouseNumbers', function(){
                if(!W.editingMediator.attributes.editingHouseNumbers)
                    init2();
            })
        }
        FUck();
    }

    function FUck() {
        log("Checking for WMEFU");
        if ($('#_cbShrinkTopBars').length) {
            initFU();
        }
        else {
        /**
        * MutationObserver for WMEFU
        */
        let targetNode = document.getElementById('user-tabs');              // Select the node that will be observed for mutations
        let config = { attributes:true, childList: true, subtree: true };  // Options for the observer (which mutations to observe)
        var callback = (function(mutations) {
                mutations.forEach(function(mutation) {
                    for (let i = 0; i < mutation.addedNodes.length; i++) {
                        let addedNode = mutation.addedNodes[i];
                        if (addedNode.nodeType === Node.ELEMENT_NODE) {
                            if (addedNode.innerText.includes("FU"))
                            {
                                initFU();
                                observer.disconnect();
                            }
                    }
                }
            });
        });
        var observer = new MutationObserver(callback);                      // Create an observer instance linked to the callback function
        observer.observe(targetNode, config);                               // Start observing the target node for configured mutations
        }
    }

    function initFU() {
        log('WMEFU - Loaded');
        //Change height based on WMEFU Settings
        if (JSON.parse(localStorage.WMEFUSettings).shrinkTopBars && $('#_inpUICompression').find(":selected").text() == "Low")
        {
            log('WMEFU - Initial Load for Low Compression');
            setTimeout(function () {
            $('#pljumpinput').css("height","26px");
            $('.pljump').css("margin","1px 10px 0px 10px");
            return;
            }, 950);
        }
        else if (JSON.parse(localStorage.WMEFUSettings).shrinkTopBars && $('#_inpUICompression').find(":selected").text() == "High")
        {
            log('WMEFU - Initial Load for High Compression');
            setTimeout(function () {
            $('#pljumpinput').css("height","19px");
            $('.pljump').css("margin","-6px 10px 0px 10px");
            return;
            }, 950);
        }
        $('#_cbShrinkTopBars').click(function(){
            if ($('#_cbShrinkTopBars').prop('checked'))
            {
                log('WMEFU - Shrinking');
                $('#pljumpinput').css("height","26px");
                $('.pljump').css("margin","1px 10px 0px 10px");
                return;
            }
            else
            {
                log('WMEFU - Restoring');
                $('#pljumpinput').css("height","40px");
                $('.pljump').css("margin","8px 10px 0px 10px");
                return;
            }
        });
        $( "#_inpUICompression" ).change(function() {
            if ($('#_inpUICompression').val() == "1")
            {
                log('WMEFU - Shrinking to Low Compression');
                $('#pljumpinput').css("height","26px");
                $('.pljump').css("margin","1px 10px 0px 10px");
                return;
            }
            else if ($('#_inpUICompression').val() == "0")
            {
                log('WMEFU - Restoring to No Compression');
                $('#pljumpinput').css("height","40px");
                $('.pljump').css("margin","8px 10px 0px 10px");
                return;
            }
            else if ($('#_inpUICompression').val() == "2")
            {
                log('WMEFU - Shrinking to High Compression');
                $('#pljumpinput').css("height","19px");
                $('.pljump').css("margin","-6px 10px 0px 10px");
                return;
            }
        });
    }

    function updateAlert() {
        var version = GM_info.script.version.toString();
        var versionChanges = '';
        var previousVersion;

        versionChanges += 'WME PL Jump v' + version + ' changes:\n';
        versionChanges += '- Style Changes\n- Compatibility Updates\n';

        if (localStorage === void 0) {
            return;
        }
        localStorage.removeItem("pljumplocation");
        localStorage.removeItem("WMEPLJLocation");
        localStorage.removeItem("pljumpLocation");


        previousVersion = localStorage.pljumpVersion;

        if (version !== previousVersion) {
            alert(versionChanges);
            localStorage.pljumpVersion = version;
        }
    }

    function initializeLink() {
        /**
         * Class representing PLs.
         */
        Link = Backbone.Model.extend({
            defaults: {
                link: '',
                lonLat: null,
                segments: null,
                mapProblem: null,
                nodes: null,
                venues: null,
                selectionInBounds: false,
                selectionOnScreen: false,
                updateRequest: null,
                zoom: null
            },
            itemsToSelect: [],
            modelObject: null,
            /**
             * Checks whether the PLs items to select are in the data bounds
             * and in view.
             */
            isSelectionOnScreen: function () {
                return this.attributes.selectionInBounds &&
                    this.attributes.selectionOnScreen;
            },
            /**
             * Checks whether the PL has items to select or not.
             */
            hasItems: function () {
                return this.attributes.segments || this.attributes.nodes ||
                    this.attributes.venues || this.attributes.updateRequest ||
                    this.attributes.mapProblem;
            },
            /**
             * Parses PL to extract the location and selected item info.
             */
            initialize: function () {
                var extractedData = {},
                    link = this.get('link'),
                    lat,
                    lon,
                    lonLat,
                    mapProblem,
                    nodes,
                    segments,
                    updateRequest,
                    venues,
                    zoom;
                var linktemp = link;
                if (linktemp.includes("google")){
                    let google = link.split('@').pop().split(',');
                    lon = google[1];
                    lat = google[0];
                    zoom = parseInt(google[2]);
                    link = 'https://www.waze.com/en-US/editor/?lon=' + lon + '&lat=' + lat + '&zoom=' + $(Math.max(0,Math.min(10,(zoom - 12))))[0];
                }
                else if (linktemp.includes("mandrillapp")){
                    let mandrillapp = link.split('_');
                    mandrillapp = window.atob(mandrillapp[1]);
                    mandrillapp = mandrillapp.split(/\\/)[0];
                    link = 'https://www.waze.com/en-US/editor/?' + mandrillapp;
                }
                else if (linktemp.includes("livemap")){
                    let livemap = link.replace("lng", "lon");
                    link = livemap;
                }

                link = decodeURIComponent(link);
                lat = link.match(/lat=(-?\d+\.\d+)/);
                lon = link.match(/lon=(-?\d+\.\d+)/);
                nodes = link.match(/nodes=((\d+,?)+)/);
                segments = link.match(/segments=((\d+,?)+)/);
                updateRequest = link.match(/mapUpdateRequest=(\d+)/);
                mapProblem = link.match(/mapProblem=(\d%2F\d+)/);
                venues = link.match(/venues=((\d+\.?)+)/);
                zoom = link.match(/zoom=(\d+)/);

                extractedData.lat = lat && lat.length === 2 ?
                    parseFloat(lat[1]) : null;
                extractedData.lon = lon && lon.length === 2 ?
                    parseFloat(lon[1]) : null;
                extractedData.segments = segments && segments.length > 1 ?
                    segments[1].split(',') : null;
                extractedData.venues = venues ? venues[1].split(',') : null;
                extractedData.nodes = nodes && nodes.length > 1 ?
                    nodes[1].split(',') : null;
                extractedData.updateRequest = updateRequest &&
                    updateRequest.length === 2 ? updateRequest[1] : null;
                extractedData.mapProblem = mapProblem &&
                    mapProblem.length === 2 ?
                    mapProblem[1].replace('%2F', '/') : null;
                extractedData.zoom = zoom && zoom.length === 2 ?
                    parseInt(zoom[1]) : null;

                this.set({
                    'segments': extractedData.segments,
                    'mapProblem': extractedData.mapProblem,
                    'nodes': extractedData.nodes,
                    'venues': extractedData.venues,
                    'updateRequest': extractedData.updateRequest,
                    'zoom': extractedData.zoom
                });

                if (extractedData.lon && extractedData.lat) {
                    lonLat = new OpenLayers.LonLat(extractedData.lon,
                                           extractedData.lat);
                    lonLat.transform(W.map.displayProjection,
                                     W.map.getProjectionObject());
                    if (W.map.isValidLonLat(lonLat)) {
                        this.set('lonLat', lonLat);
                    }
                } else {
                    this.set('lonLat', { lon: 'None', lat: 'None' });
                }

                this.on('change:link', this.onLinkChanged);
            },
            /**
             * Private method to compile WME objects for selection.
             * @private
             */
            createSelection: function () {
                var i,
                    itemsToSelect = [],
                    itemType,
                    itemTypes = ['segments', 'nodes', 'venues'],
                    mapBounds = W.map.getExtent(),
                    modelObject,
                    n,
                    selectionInBounds = true,
                    selectionOnScreen = true;

                var getObject = function (element) {
                    var object = W.model[itemType].getObjectById(element);
                    if (object) {
                        itemsToSelect.push(object);
                        if (!mapBounds.intersectsBounds(
                            object.geometry.getBounds())) {
                            selectionOnScreen = false;
                        }
                    } else {
                        selectionInBounds = false;
                    }
                };

                for (i = 0, n = itemTypes.length; i < n; i++) {
                    itemType = itemTypes[i];
                    _.each(this.attributes[itemType], getObject, this);
                }

                if (this.attributes.updateRequest ||
                    this.attributes.mapProblem) {
                    modelObject = W.model.mapUpdateRequests.getObjectById(
                        this.attributes.updateRequest) || W.model.problems.getObjectById(
                        this.attributes.mapProblem) || null;
                    if (!modelObject) {
                        selectionInBounds = false;
                    } else if (!mapBounds.intersectsBounds(
                        modelObject.attributes.geometry.getBounds())) {
                        selectionOnScreen = false;
                    }
                }

                this.set({
                    'selectionInBounds': selectionInBounds,
                    'selectionOnScreen': selectionOnScreen
                });
                this.modelObject = modelObject;
                this.itemsToSelect = itemsToSelect;
                return this;
            },
            /**
             * Pans the map to the lat & lon location specified in the PL.
             * @private
             */
            moveTo: function () {
                var zoom = this.get('zoom') || W.map.getZoom();
                if (this.attributes.lonLat) {
                    W.map.moveTo(this.attributes.lonLat, zoom);
                }
                return this;
            },
            /**
             * Public method to go to a Link and select its objects.
             */
            open: function (forceMove) {
                this.createSelection();

                if (this.hasItems()) {
                    this.select();
                    if (forceMove || !this.isSelectionOnScreen()) {
                        this.moveTo();
                    }
                } else {
                    this.moveTo();
                }

                return this;
            },
            /**
             * Re-parse the link if it changes.
             */
            onLinkChanged: function () {
                this.initialize();
            },
            /**
             * Selects objects extracted from the PL.
             * @private
             */
            select: function () {
                var selectItems = function () {

                    this.createSelection();

                    if (this.modelObject) {
                        W.commands.execute('problems:show', this.modelObject);
                    }

                    if (this.itemsToSelect.length > 0) {
                        W.selectionManager.setSelectedModels(this.itemsToSelect);
                    }
                };

                WazeWrap.Model.onModelReady(selectItems,
                                            this.isSelectionOnScreen(), this);

                return this;
            }
        });
    }

    function initializeCollection() {
        /**
         * Class representing collection of PLs.
         */
        LinkCollection = Backbone.Collection.extend({
            model: Link,
            getLinkID: function () {
                var id = 0;
                return function () { return id++; };
            } ()
        });
    }
    function initializeViews() {
        /**
         * Class containing the main app interface and logic.
         */
        AppView = Backbone.View.extend({
            collection: null,
            /**
             * Gets the stored options from localStorage and returns
             * them as an object.
             */
            options: function () {
                var defaultOptions = {
                    'trackHistory': true,
                    'trackSelections': false
                };
                var options = localStorage && localStorage.pljOptions;

                options = options && JSON.parse(options) || defaultOptions;
                return options;
            } (),
            clearButtonCss: {
                'margin-bottom': '10px'
            },
            divCss: {
                'overflow-y': 'scroll',
                'max-height': '400px'
            },
            tableDivCss: {
                'overflow-y': 'scroll',
                'max-height': '400px',
                'width': '292px'
            },
            el: $('#pljumptabcontent'),
            template: function () {
                var $div = $('<div/>'),
                    $table = $('<table/>').attr('id', 'plj-history-table'),
                    $clearButton = $('<button/>').attr('id', 'plj-clear-table').
                css(this.clearButtonCss).text('Clear all entries'),
                    $trackHistory = $('<div/>').append($('<input type="checkbox" id="plj-track-history"><label>Track map move history</label>')),
                    $trackSelections = $('<div/>').append($('<input type="checkbox" id="plj-track-selections"><label>Track selections</label>')),
                    $infoText = $('<p>Click a table entry below to select it. To force the map to move to the PL location, Ctrl+Click.</p>');

                $div.append($trackHistory);
                $div.append($trackSelections);
                $div.append($clearButton.wrap('<div>').parent());
                $div.append($infoText.wrap('<div>').parent());
                $div.append($table.wrap('<div>').parent().
                            css(this.tableDivCss));

                return $div;
            },
            events: {
                'click #plj-clear-table': 'onClearTableClicked',
                'change #plj-track-history': 'onTrackHistoryChange',
                'change #plj-track-selections': 'onTrackSelectionsChange'
            },
            initialize: function () {
                this.listenTo(this.collection, 'add', this.addLink);
                this.render();
                this.onTrackHistoryChange();
                this.onTrackSelectionsChange();
            },
            render: function () {
                this.$el.append(this.template());

                this.$trackHistory = this.$el.find('#plj-track-history');
                this.$trackHistory.prop('checked',
                                        this.options['trackHistory']);

                this.$trackSelections = this.$el.find('#plj-track-selections');
                this.$trackSelections.prop('checked',
                                           this.options['trackSelections']);

                this.$table = this.$el.find('#plj-history-table');

                this.$clearButton = this.$el.find('#plj-clear-table');

                return this;
            },
            /**
             * Adds a new link to the table.
             */
            addLink: function (link) {
                var view = new LinkView({ model: link });
                this.$table.prepend(view.render().$el);
            },
            /**
             * Tracks map moves and determines if the location changed
             * after a move. This filters out zoom-only "moves".
             */
            mapLocationChanged: function () {
                var lastLocation,
                    locationChanged,
                    newLocation;
                var getMapLocation = function () {
                    var lonLat = W.map.getCenter(),
                        zoom = W.map.getZoom();
                    return {
                        lon: lonLat.lon,
                        lat: lonLat.lat,
                        zoom: zoom
                    };
                };
                var compareLocations = function () {
                    newLocation = getMapLocation();
                    if (newLocation.lon !== lastLocation.lon ||
                        newLocation.lat !== lastLocation.lat) {
                        lastLocation = newLocation;
                        locationChanged = true;
                    } else {
                        locationChanged = false;
                    }
                };
                lastLocation = getMapLocation();
                W.map.events.register('moveend', this, compareLocations);
                return function () {
                    return locationChanged;
                };
            } (),
            /**
             * Callback for clearing the link history.
             */
            onClearTableClicked: function (e) {
                this.$table.find('tr').remove();
                this.collection.reset();
            },
            /**
             * Callback for map move.
             */
            onMapMove: function () {
                if (this.mapLocationChanged()) {
                    this.collection.add({
                        link: $('.WazeControlPermalink a').prop('href')
                    });
                }
            },
            /**
             * Callback for selection change.
             */
            onSelectionChanged: function () {
                if (this.selectionChanged()) {
                    this.collection.add({
                        link: $('.WazeControlPermalink a').prop('href')
                    });
                }
            },
            /**
             * Callback for track history checkbox change.
             */
            onTrackHistoryChange: function (e) {
                var track = this.$trackHistory.prop('checked');

                W.map.events.unregister('moveend', this, this.onMapMove);

                if (track) {
                    W.map.events.register('moveend', this, this.onMapMove);
                }

                this.saveOption('trackHistory', track);
            },
            /**
             * Callback for track selections checkbox change.
             */
            onTrackSelectionsChange: function (e) {
                var track = this.$trackSelections.prop('checked');

                W.selectionManager.events.unregister('selectionchanged', this,
                                                     this.onSelectionChanged);

                if (track) {
                    W.selectionManager.events.register('selectionchanged', this,
                                                       this.onSelectionChanged);
                }

                this.saveOption('trackSelections', track);
            },
            /**
             * Saves app options to localStorage.
             */
            saveOption: function (key, value) {
                if (localStorage === void 0) { return; }
                this.options[key] = value;
                localStorage.pljOptions = JSON.stringify(this.options);
            },
            /**
             * Tracks selection changes to determine if the same object is
             * selected consecutively.
             */
            selectionChanged: function () {
                var lastSelection,
                    newSelection,
                    selectionChanged;

                var getSelectedItem = function () {
                    return W.selectionManager.hasSelectedFeatures() &&
                        W.selectionManager.getSelectedFeatures[0];
                };

                var compareSelection = function () {
                    newSelection = getSelectedItem();
                    if (W.selectionManager.hasSelectedFeatures() &&
                        lastSelection !== newSelection) {
                        lastSelection = newSelection;
                        selectionChanged = true;
                    } else {
                        selectionChanged = false;
                    }
                };

                lastSelection = getSelectedItem();
                W.selectionManager.events.register('selectionchanged', this,
                                                   compareSelection);

                return function () {
                    return selectionChanged;
                };
            } ()
        });

        /**
         * Class for displaying link data in a table.
         */
        LinkView = Backbone.View.extend({
            tagName: 'tr',
            tdCss: {
                'padding': '5px',
                'border': '1px solid gray',
                'border-right': 'none',
                'cursor': 'pointer'
            },
            linkInfoCss: {
                'margin': 0
            },
            linkRemoveCss: {
                'padding': '5px 5px 5px 10px',
                'border': '1px solid gray',
                'border-left': 'none',
                'text-align': 'center',
                'font-weight': 'bold'
            },
            template: function () {
                var $nameCell = $('<td/>').
                css(this.tdCss).addClass('plj-link'),

                    $deleteCell = $('<td/>').
                css(this.linkRemoveCss).
                addClass('plj-remove-link').
                append($('<a/>').text('X')),

                    attributes = this.model.attributes,
                    lonLat,
                    objectsText = [];

                if (attributes.lonLat) {
                    lonLat = attributes.lonLat.clone();
                    lonLat.transform(W.map.getProjectionObject(),
                                     W.map.displayProjection);
                }

                objectsText.push('<b>Lon:</b> ' + (lonLat ?
                                                   lonLat.lon.toFixed(3) + '\xB0' : 'None') +
                                 '  <b>Lat:</b> ' + (lonLat ? lonLat.lat.toFixed(3) + '\xB0' : 'None') +
                                 '  <b>Zoom:</b> ' + (attributes.zoom ? attributes.zoom : 'None'));

                if (this.model.hasItems()) {
                    if (attributes.updateRequest) {
                        objectsText.push('<b>Update Request:</b> ' +
                                         attributes.updateRequest);
                    }
                    if (attributes.mapProblem) {
                        objectsText.push('<b>Map Problem:</b> ' +
                                         attributes.mapProblem);
                    }
                    if (attributes.segments) {
                        objectsText.push('<b>Segments:</b> ' +
                                         attributes.segments.join(', '));
                    }
                    if (attributes.nodes) {
                        objectsText.push('<b>Nodes:</b> ' +
                                         attributes.nodes.join(', '));
                    }
                    if (attributes.venues) {
                        objectsText.push('<b>Places:</b> ' +
                                         attributes.venues.join(', '));
                    }
                }

                _.each(objectsText, function (text) {
                    $nameCell.append(
                        //$('<p/>').css(this.linkInfoCss).html(text)
                        $('<p/>').html(text)
                    );
                }, this);

                return [$nameCell, $deleteCell];
            },
            events: {
                'click .plj-link': 'onLinkClicked',
                'click .plj-remove-link': 'onRemoveClicked'
            },
            initialize: function () {
                this.listenTo(this.model, 'change', this.render);
                this.listenTo(this.model, 'destroy', this.remove);
            },
            render: function () {
                var cells = this.template();

                this.$nameCell = cells[0];
                this.$deleteCell = cells[1];

                this.$el.empty();
                this.$el.append(this.$nameCell).append(this.$deleteCell);
                return this;
            },
            /**
             * Callback for clicking a link.
             */
            onLinkClicked: function (e) {
                var forceMove = e && e.ctrlKey;

                W.map.events.unregister('moveend', app, app.onMapMove);

                this.model.open(forceMove);
                app.onTrackHistoryChange();
            },
            /**
             * Callback for removing a link.
             */
            onRemoveClicked: function (e) {
                this.model.destroy();
            }
        });

        /**
         * View for text input box and buttons for manual PL input.
         */
        JumpView = Backbone.View.extend({
            collection: null,
            tagName: 'div',
            template: function () {
                var buttonstyle = '';
                var inputstyle = "";
                return `<input type="text" id="pljumpinput" style="font-family:'Rubik', 'Boing-light', sans-serif,FontAwesome; display: inline-block; line-height: normal; outline:0px; box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.05); transition: 0.5s all; background-color: #f0f4f6; border-radius: 8px; border: none; padding: 4px 6px 4px 6px; color: #354148; opacity: 1;height:40px;" placeholder="&#xf0c1; WME PL Jump"></input>` +
                    '<button id="pljumpbutton-move" class="btn btn-primary" style="margin-bottom: 5px;margin-right: 4px;margin-left: 4px;padding-left:5px;padding-right:5px;" title="Select & Jump"><i class="fa fa-hand-pointer-o" aria-hidden="true"></i></button>';
                //'<button id="pljumpbutton" class="btn btn-primary" style="margin-bottom: 5px; padding-left:5px; padding-right:5px;" title="Select & Jump"><i class="fa fa-rocket" aria-hidden="true"></i></button>';
            },
            events: {
                'click #pljumpbutton': 'onJumpClick',
                'click #pljumpbutton-move': 'onJumpMoveClick',
                'keyup #pljumpinput': 'onInputChanged'
            },
            initialize: function () {
                this.render();
                this.onInputChanged();
            },
            render: function () {
                this.$el.html(this.template());
                this.$el.addClass("pljump");
                this.$el.css({
                    'float': 'left',
                    'width':'213px',
                    'margin': '8px 10px 0px 10px'
                });

                this.input = this.$el.find('#pljumpinput');
                this.jumpButton = this.$el.find('#pljumpbutton');
                this.jumpMoveButton = this.$el.find('#pljumpbutton-move');
                // Puts it in the topbar
                $('#edit-buttons > div').prepend(this.$el);
              },
            /**
             * Callback for clicking the jump button.
             */
            onJumpClick: function (e, forceMove) {
                var linkText = this.input.val(),
                    newLink;

                if (linkText) {
                    newLink = this.collection.add({ link: linkText });
                    newLink.open(forceMove);
                }
                this.input.val('');
                this.onInputChanged();
            },
            /**
             * Callback for clicking the jump and move button.
             */
            onJumpMoveClick: function (e) {
                this.onJumpClick(e, true);
            },
            /**
             * Callback for keyup in the input box.
             * Disables the buttons if the textbox is empty.
             * If the key is 'enter', triggers the jump button click.
             */
            onInputChanged: function (e) {
                /*
                this.jumpButton.prop('disabled',
                    this.input.val() === '' ? true : false);
                this.jumpMoveButton.prop('disabled',
                    this.input.val() === '' ? true : false);
                */
                if (e && e.keyCode === 13) {
                    this.onJumpClick();
                }
            }
        });
    }
    bootstrap();
} ());