Can Firefox addon panel determine when it is shown and hidden? - javascript

I am using the Tool API to add a panel to Firefox DevTools.
I can define setup() and dispose() methods to handle initialization and teardown.
However I can’t figure out how to determine whether the panel is currently visible, or when it changes visibility. Is this event exposed somewhere?
To be clear, I only want to know that for my panel. So I want to know when my panel becomes visible, or when the user switches away to e.g. the Elements tab.

The dev/panel API does not expose a method for you to be notified when the visibility of your panel changes. However, you can go around the API and be informed that the visibility has changed.
The code below calls the function panelVisibilityChangedState when the visibility changes of the panel created by the extension within the Developer Tools Toolbox. As requested, this is only the state of the panel added by the extension. This add-on was tested while running multiprocess Firefox Developer Edition, version 50.0a2.
This code is based on the MDN repl-panel example available on GitHub.
main.js:
//This code is based on the MDN Dev Tools panel example available at:
// https://github.com/mdn/repl-panel
//Require the needed SDK modules
const { Panel } = require("dev/panel");
const { Tool } = require("dev/toolbox");
const { Class } = require("sdk/core/heritage");
const self = require("sdk/self");
var myRadio;
var devToolsToolbar;
var devToolsToolboxTabs;
var pickerContainer;
var panelIsVisible=false;
function panelVisibilityChangedState(isVisible){
//This function may be called slightly before the state change actually occurs.
panelIsVisible=isVisible;
console.log('Dev Tools Panel Changed State: visibility is now: ' + isVisible );
}
function devToolsClickHandler(event){
//The event fires before the value of the 'selected' attribute changes in response to
// this click, except when the event fires on the pick element. In that case, the
// 'selected' attribute is accurately 'false'.
let isSelected = myRadio.getAttribute('selected') === 'true';
let pickElementContains = pickerContainer.contains(event.target);
if(!devToolsToolboxTabs.contains(event.target) && !pickElementContains){
//Of the controls not in the devToolsToolboxTabs, only the pickElement changes
// the state of this panel being shown. The exception to this is the close
// button, but closing is detected via the panel's dispose method.
return;
}//else
let doesContain = myRadio.contains(event.target);
if((pickElementContains && panelIsVisible)
|| (doesContain && !isSelected) || (!doesContain && isSelected)) {
panelVisibilityChangedState(doesContain);
}
}
//Define a REPLPanel class that inherits from dev/panel
const REPLPanel = Class({
extends: Panel,
label: "Visi",
tooltip: "Demo Dev Tool Panel Visibility Notification",
icon: self.data.url("noIcon.png"),
url: self.data.url("blank-panel.html"),
setup: function(options) {
//Remember the button which was clicked to open this panel (actually a <radio>)
myRadio = require("sdk/window/utils").getFocusedElement()
//For convenience and to speed up the event handler, remember three elements.
// Obtain these elements using IDs, or unique class when no ID is available.
// This should be a bit more stable than using the location in the DOM
// relative to myRadio.
devToolsToolbar = myRadio.ownerDocument.querySelector('.devtools-tabbar');
//An alternate method of finding the Dev Tools toolbar:
//devToolsToolbar = myRadio.ownerDocument.getElementById('toolbox-tabs').parentNode;
//Another alternate method of finding the Dev Tools toolbar:
//devToolsToolbar = myRadio.parentNode.parentNode;
devToolsToolboxTabs = devToolsToolbar.querySelector('#toolbox-tabs');
pickerContainer = devToolsToolbar.querySelector('#toolbox-picker-container');
devToolsToolbar.addEventListener('click',devToolsClickHandler,false);
},
dispose: function() {
//Dev Tools panel is destroyed. Visibility is, obviously, false
if(panelIsVisible){
panelVisibilityChangedState(false);
}
},
onReady: function() {
//The panel is now visible and ready. If desired, this call to
// panelVisibilityChangedState could be placed in the 'setup' function.
panelVisibilityChangedState(true);
}
});
exports.REPLPanel = REPLPanel;
//Create a new tool, initialized with the REPLPanel's constructor
const replTool = new Tool({
panels: { repl: REPLPanel }
});
data/blank-panel.html:
<html>
<head>
<meta charset="utf-8">
</head>
<body>
This is a blank panel
</body>
</html>
package.json:
{
"name": "dev-panel-visibility-notification",
"title": "dev-panel-visibility-notification",
"id": "dev-panel-visibility-notification#example",
"description": "Demonstrates calling a function when the visibillity of the add-on's Dev Tools panel changes.",
"author": "Makyen",
"license": "MPL 2.0",
"version": "0.1.0",
"main": "main.js"
}

Related

SVG content disappearing from IFrame documentContent

I'm attempting to replicate the content in a particular IFrame element inside of a modal to avoid unnecessary DB calls. I am invoking a clientside callback via Python (see here) that returns the index of a particular IFrame element I would like to replicate in my modal.
Here is the snippet of Python code that toggles my modal and tracks the index of the most recently clicked figure to replicate:
#app.callback(
[Output('my-modal', 'is_open'),
Output('modal-clone', 'children')],
[Input(f'button{k}', 'n_clicks_timestamp') for k in range(20)] +
[State('my-modal', 'is_open')])
def toggle_modal(*data):
clicks, is_open = data[:20], data[20]
modal_display = not is_open if any(clicks) else is_open
clicked = clicks.index(max(clicks))
return [modal_display, clicked]
app.clientside_callback(
ClientsideFunction(namespace='clientside', function_name='clone_figure'),
Output('modal-test', 'children'),
[Input('modal-clone', 'children'), Input('modal-figure', 'id')]
)
And the following Javascript:
window.dash_clientside = Object.assign({}, window.dash_clientside, {
clientside: {
clone_figure: function(clone_from, clone_to) {
source = document.getElementById(clone_from);
console.log(document.getElementById(clone_to))
console.log(document.getElementById(clone_to).contentDocument);
clone = document.getElementById(clone_to);
// set attributes of clone here using attributes from source
return null
}
}
});
Now, from my console.log() statements, I noticed the following (note that modal-clone in the screenshot corresponds to modal-figure in my example):
How is contentDocument changing between these two log statements? Any insight would be greatly appreciated, I am stumped.
It appears that you need to addEventListener() to the IFrame element:
clone_spray: function(clone_from, clone_to) {
source = document.getElementById(clone_from);
clone = document.getElementById(clone_to);
if (!clone) {return null;}
clone.addEventListener("load", function() {
// set attributes of clone here using attributes from source

How do I detect the first time the user logs in and the first time a specific page is loaded?

I would like to Trigger some JS only the first time a user logs in, and only the first time a specific page is loaded.
I believe I can deal with the first time they log in, by simply checking user.sign_in_count < 2, but I don't know how to specify just on the first page load only.
i.e. I don't want the JS to be triggered after the user logs in for the first time and refreshes the page without logging out.
I am using Turbolinks and $(document).on('turbolinks:load', function() { to trigger it.
Edit 1
So what I am trying to do is execute Bootstrap Tour on a number of pages. But I only want that tour to be automatically executed, on the first page load. The tour itself will lead the user to other specific pages within my app, but each of those pages will have page-specific tour JS on each page.
Right now, in my HTML I have something like this:
<script type="text/javascript">
$(document).on('turbolinks:load', function() {
var tour = new Tour({
storage: false,
backdrop: true,
onStart: function(){
$('body').addClass('is-touring');
},
onEnd: function(){
$('body').removeClass('is-touring');
},
steps: [
{
element: "#navbar-logo",
title: "Go Home",
content: "All throughout the app, you can click our logo to get back to the main page."
},
{
element: "input#top-search",
title: "Search",
content: "Here you can search for players by their name, school, positions & bib color (that they wore in our tournament)"
}
]});
// Initialize the tour
tour.init();
// Start the tour
tour.start();
});
</script>
So all I really want to do is the following:
Not bombard the user with executing a new tour, on their first login, whenever they reload the page.
Allow them to be able to manually execute the tour at a later date if they want, by simple pressing a link.
I don't want to store anything in my DB if I don't have to -- so preferably this should be a cookie-based approach or localStorage
Assume that I will use Rails to track the number of sign-ins they have done. So once they sign in more than once, I can not trigger this JS.
The real problem is just within that first sign in, if they refresh the main page 10 times, this tour gets executed 10 times. That's what I am trying to stop.
I hope that provides some more clarity.
Preface
It's my understanding that you have:
multiple pages that contain a single tour (each page's tour is different)
a way to detect first signin to an account (ruby login count)
ability to add a script value based upon first signin
Solution Overview
The solution below uses localStorage to store a key value pair of each tour's identifier and if it has been seen or not. localStorage persists between page refreshes and sessions, as the name suggests, localStorage is unique to each domain, device, and browser (ie. chrome's localStorage cannot access firefox's localStorage even for the same domain, nor can chrome's localStorage on your laptop access chrome's localStorage on your mobile even for the same domain). I raise this to illustrate the reliance upon Preface 3 to toggle a JS flag for if the user has logged in previously.
For the tour to start, the code checks localStorage for if its corresponding key value pair is not set to true (representing having been "seen"). If it does exist and is set to true, the tour does not start, otherwise it runs. When each tour begins, using its onStart method, we update/add the tour's identifier to localStorage and set its value to true.
Manual execution of the tour can be performed by either manually calling the tour's start method if you would like only the current page's tour to execute, otherwise, you can clear out all of the localStorage related to the tour and send the user back to the first page/if you're on the first page, again just call the start method.
JSFiddle (HTML based off other question's you've asked regarding touring)
HTML (this could be any element with the id="tourAgain" attribute for the following code to work.
<button class="btn btn-sm btn-default" id="tourAgain">Take Tour Again</button>
JS
var isFirstLogin = true; // this value is populated by ruby based upon first login
var userID = 12345; // this value is populated by ruby based upon current_user.id, change this value to reset localStorage if isFirstLogin is true
// jquery on ready function
$(function() {
var $els = {}; // storage for our jQuery elements
var tour; // variable that will become our tour
var tourLocalStorage = JSON.parse(localStorage.getItem('myTour')) || {};
function activate(){
populateEls();
setupTour();
$els.tourAgain.on('click', tourAgain);
// only check check if we should start the tour if this is the first time we've logged in
if(isFirstLogin){
// if we have a stored userID and its different from the one passed to us from ruby
if(typeof tourLocalStorage.userID !== "undefined" && tourLocalStorage.userID !== userID){
// reset the localStorage
localStorage.removeItem('myTour');
tourLocalStorage = {};
}else if(typeof tourLocalStorage.userID === "undefined"){ // if we dont have a userID set, set it and save it to localStorage
tourLocalStorage.userID = userID;
localStorage.setItem('myTour', JSON.stringify(tourLocalStorage));
}
checkShouldStartTour();
}
}
// helper function that creates a cache of our jQuery elements for faster lookup and less DOM traversal
function populateEls(){
$els.body = $('body');
$els.document = $(document);
$els.tourAgain = $('#tourAgain');
}
// creates and initialises a new tour
function setupTour(){
tour = new Tour({
name: 'homepage', // unique identifier for each tour (used as key in localStorage)
storage: false,
backdrop: true,
onStart: function() {
tourHasBeenSeen(this.name);
$els.body.addClass('is-touring');
},
onEnd: function() {
console.log('ending tour');
$els.body.removeClass('is-touring');
},
steps: [{
element: "div.navbar-header img.navbar-brand",
title: "Go Home",
content: "Go home to the main page."
}, {
element: "div.navbar-header input#top-search",
title: "Search",
content: "Here you can search for players by their name, school, positions & bib color (that they wore in our tournament)"
}, {
element: "span.num-players",
title: "Number of Players",
content: "This is the number of players that are in our database for this Tournament"
}, {
element: '#page-wrapper div.contact-box.profile-24',
title: "Player Info",
content: "Here we have a quick snapshot of the player stats"
}]
});
// Initialize the tour
tour.init();
}
// function that checks if the current tour has already been taken, and starts it if not
function checkShouldStartTour(){
var tourName = tour._options.name;
if(typeof tourLocalStorage[tourName] !== "undefined" && tourLocalStorage[tourName] === true){
// if we have detected that the tour has already been taken, short circuit
console.log('tour detected as having started previously');
return;
}else{
console.log('tour starting');
tour.start();
}
}
// updates localStorage with the current tour's name to have a true value
function tourHasBeenSeen(key){
tourLocalStorage[key] = true;
localStorage.setItem('myTour', JSON.stringify(tourLocalStorage));
}
function tourAgain(){
// if you want to tour multiple pages again, clear our localStorage
localStorage.removeItem('myTour');
// and if this is the first part of the tour, just continue below otherwise, send the user to the first page instead of using the function below
// if you just want to tour this page again just do the following line
tour.start();
}
activate();
});
PS. the reason we dont use onEnd to trigger the tourHasBeenSeen function is that there is currently a bug with bootstrap tour where if the last step's element doesnt exist, the tour ends without triggering the onEnd callback, BUG.
You could try using Javascript's sessionStorage, which is deleted when the user closes the tab, but survives through refreshes. Just use sessionStorage.setItem(key, value and sessionStorage.getItem(key). Remember that sessionStorage can only store strings!
Using your code:
<script type="text/javascript">
$(document).on('turbolinks:load', function() {
var tour = new Tour({
storage: false,
backdrop: true,
onStart: function(){
$('body').addClass('is-touring');
},
onEnd: function(){
$('body').removeClass('is-touring');
},
steps: [
{
element: "#navbar-logo",
title: "Go Home",
content: "All throughout the app, you can click our logo to get back to the main page."
},
{
element: "input#top-search",
title: "Search",
content: "Here you can search for players by their name, school, positions & bib color (that they wore in our tournament)"
}
]});
if(sessionStorage.getItem("loggedIn") !== "yes"){//Remember that sessionStorage can only store strings!
//Initialize the tour
tour.init();
// Start the tour
tour.start();
}
else{
//Set item "loggedIn" in sessionStorage to "yes"
sessionStorage.putItem("loggedIn", "yes");
}
var goBackToTour = function(e){
//You can also make a "fake" link, so that it looks like a link, but is not, and you don't have to put the following line:
e.preventDefault();
tour.init();
tour.start();
};
document.getElementById("goBackToTourLink").addEventListener("click", goBackToTour);
});
//On the logout
var logout = function(){
sessionStorage.setItem("loggedIn", "no");
};
</script>
You can store if user has seen the tour or not in the cookie. You can maintain a "TrackingCookie" which has all the user tracking information (eg. tour_shown, promotion_shown etc, which is accessed by your javascript
code. Following TrackingCookie code is to maintain all such tracking information in one cookie. I am calling it tracking_cookie.
Cookies can be accessed server-side using
cookies[:tracking_cookie]
tracking_cookie.js
var TrackingCookie = (function() {
function TrackingCookie() {
this.name = 'tracking_cookie';
this.expires = new Date(new Date().setYear(new Date().getFullYear() + 1));
}
TrackingCookie.prototype.set = function(name, value) {
var data={};
if(!this.readFromStore()) {
data = this.readFromStore();
}
data[name] = value;
return this.writeToStore(data);
};
TrackingCookie.prototype.set_if_unset = function(name, value) {
if (!this.get(name)) {
return this.set(name, value);
}
};
TrackingCookie.prototype.get = function(name) {
return this.readFromStore()[name];
};
TrackingCookie.prototype.writeToStore = function(data) {
return $.cookie(this.name, JSON.stringify(data), {
path: '/',
expires: this.expires
});
};
TrackingCookie.prototype.readFromStore = function() {
return $.parseJSON($.cookie(this.name));
};
return TrackingCookie;
})();
In your HTML
<script type="text/javascript">
$(document).on('turbolinks:load', function() {
//Instantiate the cookie
var tracking_cookie = new TrackingCookie();
//Cookie value not set means, it is a new user.
if(!tracking_cookie.get("tour_shown")){
//Set the value to be true.
tracking_cookie.set("tour_shown",true)
var tour = new Tour({
storage: false,
backdrop: true,
onStart: function(){
$('body').addClass('is-touring');
},
onEnd: function(){
$('body').removeClass('is-touring');
},
steps: [
{
element: "#navbar-logo",
title: "Go Home",
content: "All throughout the app, you can click our logo to get back to the main page."
},
{
element: "input#top-search",
title: "Search",
content: "Here you can search for players by their name, school, positions & bib color (that they wore in our tournament)"
}
]});
// Initialize the tour
tour.init();
// Start the tour
tour.start();
};
});
</script>
The cookie class is verbose. You can just use $.cookie to achieve simple one toggle behavior. The above code works for all first time users, logged-in as well as logged-out. If you just want it for logged-in user, set the flag on user log-in on server-side.
To use local storage:
if (typeof(Storage) !== "undefined") {
var takenTour = localStorage.getItem("takenTour");
if (!takenTour) {
localStorage.setItem("takenTour", true);
// Take the tour
}
}
We use this solution because our users don't log in, and it is a bit lighter than using cookies. As mentioned above it doesn't work when users switch machines or clear the cache, but you have that covered off by your login count.
Based on your comment, I think you're going to want to track this in your data (which is effectively what you're doing with the user.sign_in_count > 1 check). My recommendation would be to use a lightweight key-value data store like Redis.
In this model, each time a user visits a page that has this feature, you check for a "visited" value associated with that user in Redis. If it doesn't exist, you trigger the JS event and add "visited": true to Redis for that user, which will prevent the JS from triggering in the future.
Local storage is not a cross browser solution. Try this cross browser SQL implementation which uses different methods (including localstorage) to store 'databases' on the users hard drive indefinitely.
var visited;
jSQL.load(function(){
// create a table
jSQL.query("create table if not exists visits (time date)").execute();
// check if the user visited
visited = jSQL.query("select * from visits").execute().fetchAll("ASSOC").length;
// update the table so we know they visited already next time
jSQL.query("insert into visits values (?)").execute([new Date()]);
jSQL.persist();
});
This should work if what you want to do is gate the page for its life. If you need to prevent re-execution for longer periods, consider localStorage.
var triggered;
$(document).on('turbolinks:load', function() {
if (triggered === undefined) {
triggered = "yes";
...code...
}}
You're going to have to communicate with the backend somehow to get sign-in count. Either in a injected variable, or as json route you hit with ajax, do logic like:
if !session[:seen_tour] && current_user.sign_in_count == 1
#show_tour = true
session[:seen_tour] = true
else
#show_tour = false
end
respond_to do |format|
format.html {}
format.json { render json: {show_tour: #show_tour } }
end
Values in session will persist however you've configured your session store, by default that is stored in cookies.

Cannot locate element using recursion after it found it as visible

My Problem:
I am trying to click options in a dropdown with Nightwatch, using sections in page objects. I'm not sure if it's a problem with the section declaration or i'm missing something scope-related. Problem is that it finds the element as visible, but when it tries to click it will throw error that it cannot locate it using recursion.
What could i try to do to fix this issue using sections?
In the test:
var myPage = browser.page.searchPageObject();
var mySection = searchPage.section.setResults;
// [finding and clicking the dropdown so it opens and displays the options]
browser.pause (3000);
browser.expect.section('#setResults').to.be.visible.before(1000);
myPage.myFunction(mySection, '18');
In the page object:
var searchKeywordCommands = {
myFunction: function (section, x) {
section.expect.element('#set18').to.be.visible.before(2000);
if (x == '18') section.click('#set18');
//[...]
};
module.exports = {
//[.. other elements and commands..]
sections: {
setResults: {
selector: '.select-theme-result', //have also tried with '.select-content' and '.select-options' but with the same result
elements: {
set18: '.select-option[data-value="18"]',
set36: '.select-option[data-value="36"]' //etc
}}}}
Here is my source code:
When i run this piece of core, it seems to find the section, finds the element visible (i also can clearly see that it opens the dropdown and shows the options) but when trying to click any option, i get the error: ERROR: Unable to locate element: Section[name=setResults], Element[name=#set18]" using: recursion
Here is the full error:
My attempts:
I have tried to declare that set18 selector as an individual element instead of inside of the section and everything works fine this way, but won't work inside of the section. I have also tried all the selectors available to define the section's selector, but it won't work with any of them.
This is what i am doing with(LOL)
I assume steps would be (find dropbox - click dropbox - select value).
var getValueElement = {
getValueSelector: function (x) {
return 'li[data-value="'+ x + '"]';
}
};
module.exports = {
//[.. other elements and commands..]
sections: {
setResults: {
commands:[getValueElement],
selector: 'div[class*="select-theme-result"', //* mean contains,sometime class is too long and unique,also because i am lazy.
elements: {
setHighlight:'li[class*="select-option-highlight"]',
setSelected:'li[class*="select-option-selected"]',
//set18: 'li[data-value="18"]',
//set36: 'li[data-value="36"]'
// i think getValueFunction is better,what if you have 100+ of set.
}}}}
In your test
var myPage = browser.page.searchPageObject();
var mySection = searchPage.section.setResults;
// [finding and clicking the dropdown so it opens and displays the options]
mySection
.click('#dropboxSelector')
.waitForElementVisible('#setHighlight',5000,false,
function(){
var set18 = mySection.getValueElement(18);
mySection.click(set18);
});
Ps:in my case(i think your case also), dropbox or any small third-party js framework which is used many times in your web app, so better create a different PageObject for it,make pageObject/section is simple as possible.

How to hide some issue link types in the Issue Link pop up window for Jira 5.1.8 using javascript?

I wanted to hide some issue link outward & inwards strings of Link type from the Link Issues Popup Window using java script.
I have tried using java script but I am not getting the popup screen from the java script.
Please see the screenshot below :
Can anyone tell me how can I get this popup screen in the java script?
Is there any other method to hide this?
Thanks & Regards,
Renuka.
To hide the clone issue link every page:
edit the file system-webresources-plugin.xml (should be at /atlassian-jira/WEB-INF/classes/), and add to <web-resource key="jira-fields"> this code:
<resource type="download" name="myScript.js" location="/includes/jira/field/script.js">
<param name="source" value="webContextStatic"/>
</resource>
than, on /includes/jira/field/myScript.js write this:
AJS.$(document).ready(function() {
if (AJS.$("#link-type option[value*='clon']").size() > 0) {
// will work even when right clicking on More
// Actions->Link & open it into a new window
AJS.$("#link-type option[value*='clon']").remove()
} else if (AJS.$("#link-issue").size() > 0) {
// will work in case the link menu showing via popup
AJS.$("#link-issue").click(function(){
// wait for the popup to show, and remove the clone options
setTimeout(function (){
AJS.$("#link-type option[value*='clon']").remove();
}, 300);
});
}
});
restart Jira and it that it!
The script attaches a function to the link-menu opening, than gives the menu 0.3 seconds to load, and removes the unwanted items. If it doesn't work well for you, try to raise the timeout from 300 to 500-1000.
On jira 4, run instead:
AJS.$("#issue-link-link-type option[value*='clon']").remove();
The previous solution has an issue:
It will only work when clicking the "Link Issue"-Menu-Item.
When I use the Point (.)-Shortcut-Menu, it won't remove the issue types.
I have established the following solution:
JS-Binding-Part:
AJS.$(document).ready(function() {
JIRA.bind(JIRA.Events.NEW_CONTENT_ADDED, function(e, context, reason) {
hideIssueLinkTypes();
});
});
JS-Backing-Function:
function hideIssueLinkTypes() {
var apiURL = "/rest/scriptrunner/latest/custom/getHiddenLinkTypes"
$.getJSON( apiURL, {
}).done(function( objectData ) {
$.each( objectData, function( i, item ) {
var issueLinkType = item.issueLinkType[0];
AJS.$("#link-type option[value='"+issueLinkType.inwardDescription+"']").remove();
AJS.$("#link-type option[value='"+issueLinkType.outwardDescription+"']").remove();
});
});
}
Scriptrunner-REST-Endpoint:
import com.onresolve.scriptrunner.runner.rest.common.CustomEndpointDelegate
import groovy.json.JsonBuilder
import groovy.transform.BaseScript
import com.atlassian.jira.issue.link.DefaultIssueLinkTypeManager
import com.atlassian.jira.issue.link.IssueLinkTypeManager
import com.atlassian.jira.issue.link.IssueLinkType
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.config.properties.ApplicationProperties
import javax.ws.rs.core.MultivaluedMap
import javax.ws.rs.core.Response
#BaseScript CustomEndpointDelegate delegate
String HIDDEN_IDENT="[hidden]"
getHiddenLinkTypes(httpMethod: "GET") { MultivaluedMap queryParams, String body ->
def appProperties = ((ApplicationProperties) ComponentAccessor.getComponentOfType(ApplicationProperties.class));
def appClonersLinkTypeName = appProperties.getDefaultBackedText("jira.clone.linktype.name");
def jsBuilder=new JsonBuilder();
def issueLinkTypes = ((IssueLinkTypeManager) ComponentAccessor.getComponentOfType(IssueLinkTypeManager.class)).getIssueLinkTypes();
jsBuilder issueLinkTypes.findAll({it.getName().contains(HIDDEN_IDENT) || it.getName()==appClonersLinkTypeName }),
{ IssueLinkType linkType ->
issueLinkType linkType.getId(),
name: linkType.getName(),
inwardDescription: linkType.getInward(),
outwardDescription: linkType.getOutward()
}
return Response.ok(jsBuilder.toString()).build();
}
What you can do then ist just annotate and Link-Type with putting [hidden] in the link name and it will disappear for all users (It can still be programmatically added though or created by cloning).
If you don't have Scriptrunner or don't need the dynamic nature of the implementation, you can still hard-code the values as Kuf described in the answer above in hideIssueTypes() like this:
AJS.$("#issue-link-link-type option[value*='clon']").remove();

How to change dojo content tab focus when closing a tab to focus on previous tab instead of first tab?

I'm using Dojo's Dijit Layout for generating content tab-panes similar to Dijit Theme Tester Demo. All of the tabs here are closeable.
The issue is: when I close a tab it goes back to the first tab in the list instead of the previous tab (available next to it).
You can think of it like opening a new tab in Firefox or Chrome and try closing the last tab.... on closing tab, it changes the focus to the previous tab which is a predictable behavior for working with tabs. But with dijit.TabContainers, by default it goes back to the very first tab instead of previous one. This is a serious flaw when you consider the UI basics.
I have checked with dojo docs, but don't found any hint on this. Any idea how to it?
Ok so when the [X] button on the dijit.layout.ContentPane (tab) is clicked an event onClose is generated, the dijit.layout.TabContainer is listening to this event, so when this happens, it executes the callback closeChild() then the function removeChild() is executed, this last one is the one you should override.
The tabContainer inherits this two functions from dijit.layout.StackContainer you should check the API documentation.
So for being able to modify the default behavior of the closing tabs, you should override the default functionality, in the example below i do this. Read the comments for info on where to add your logic.
E.g.
<script>
require([
"dojo/parser",
"dojo/_base/lang", //this is the one that has the extend function
"dojo/topic", //this is needed inside the overrided function
"dijit/layout/TabContainer",
"dijit/layout/ContentPane",
"dojo/domReady!"
], function(Parser, lang, topic, tabContainer, contentPane){
Parser.parse();
// this will extend the tabContainer class and we will override the method in question
lang.extend(tabContainer, {
// this function, i copied it from the dijit.layout.StackContainer class
removeChild: function(/*dijit._Widget*/ page){
// for this to work i had to add as first argument the string "startup"
this.inherited("startup", arguments);
if(this._started){
// also had to call the dojo.topic class in the require statement
topic.publish(this.id + "-removeChild", page);
}
if(this._descendantsBeingDestroyed){ return; }
if(this.selectedChildWidget === page){
this.selectedChildWidget = undefined;
if(this._started){
var children = this.getChildren();
if(children.length){
// this is what you want to add your logic
// the var children (array) contains a list of all the tabs
// the index selects the tab starting from left to right
// left most being the 0 index
this.selectChild(children[0]);
}
}
}
if(this._started){
this.layout();
}
}
});
// now you can use your modified tabContainer WALAAAAAAA!
// from here on, normal programmatic tab creation
var tc = new tabContainer({
style: "height: 100%; width: 100%;",
}, "tab-container-div-id");
var cp1 = new contentPane({
title: "First Tab",
closable: true
});
tc.addChild(cp1);
var cp2 = new contentPane({
title: "Second Tab",
closable: true
});
tc.addChild(cp2);
var cp3 = new contentPane({
title: "Third Tab",
selected: true,
closable: true
});
tc.addChild(cp3);
tc.startup();
});
</script>
In Dojo 1.10, reverting to the previous tab is the normal behaviour for TabContainers (instead of reverting to the first tab).
Presumably, you could use dojo/aspect to get the old behaviour (warning: not tested):
require( [ 'dijit/registry', 'dojo/aspect', 'dojo/_base/lang' ],
function( registry, aspect, lang )
{
var tabContainer = registry.byId( 'tab_container');
aspect.before( tabContainer, 'removeChild', lang.hitch( tabContainer, function( page )
{
if(this.selectedChildWidget === page)
{
this.selectedChildWidget = undefined;
if(this._started)
{
var children = this.getChildren();
this.selectChild( children[0] );
}
}
return page;
} ) );
}
);
Or, alternatively, you could use the onClose extension point on a tab's ContentPane:
require( [ 'dijit/registry', 'dojo/_base/lang' ],
function( registry, lang ) {
newTabPane.onClose = lang.hitch(this, function () {
// select first
var tabContainer = registry.byId('tab_container'),
all_tabs = tabContainer.getChildren();
tabContainer.selectChild( all_tabs[0] );
// allow save to go ahead
return true;
});
}
);
Obviously, both these approaches would allow you to select a specific different pane on a tab being closed instead with a little tweaking...

Categories