summaryrefslogtreecommitdiffstats
path: root/client
diff options
context:
space:
mode:
Diffstat (limited to 'client')
-rw-r--r--client/components/activities/comments.js7
-rw-r--r--client/components/main/editor.jade7
-rwxr-xr-xclient/components/main/editor.js234
-rw-r--r--client/components/settings/peopleBody.jade4
-rw-r--r--client/components/sidebar/sidebar.jade9
-rw-r--r--client/components/users/userHeader.jade12
-rw-r--r--client/lib/popup.js204
7 files changed, 330 insertions, 147 deletions
diff --git a/client/components/activities/comments.js b/client/components/activities/comments.js
index 50ca019b..e885459e 100644
--- a/client/components/activities/comments.js
+++ b/client/components/activities/comments.js
@@ -33,6 +33,13 @@ BlazeComponent.extendComponent({
cardId,
});
resetCommentInput(input);
+ // With Richer editor is in use, and comment is submitted,
+ // clear comment form with JQuery. Id #summernote is defined
+ // at client/components/main/editor.jade where it previously was
+ // id=id, now it is id="summernote".
+ if (Meteor.settings.public.RICHER_CARD_COMMENT_EDITOR === 'true') {
+ $('#summernote').summernote('code', '');
+ }
Tracker.flush();
autosize.update(input);
input.trigger('submitted');
diff --git a/client/components/main/editor.jade b/client/components/main/editor.jade
index dbd61715..5c5454ee 100644
--- a/client/components/main/editor.jade
+++ b/client/components/main/editor.jade
@@ -1,8 +1,13 @@
template(name="editor")
+ // With Richer editor is in use, and comment is submitted,
+ // clear comment form with JQuery Comment at
+ // client/components/activities/comments.js . Id #summernote is defined
+ // here at client/components/main/editor.jade where it previously was
+ // id=id, now it is id="summernote".
textarea.editor(
dir="auto"
class="{{class}}"
- id=id
+ id="summernote"
autofocus=autofocus
placeholder="{{_ 'comment-placeholder'}}")
+Template.contentBlock
diff --git a/client/components/main/editor.js b/client/components/main/editor.js
index 39c03aa9..3f09d284 100755
--- a/client/components/main/editor.js
+++ b/client/components/main/editor.js
@@ -1,87 +1,3 @@
-import _sanitizeXss from 'xss';
-const ASIS = 'asis';
-const sanitizeXss = (input, options) => {
- const defaultAllowedIframeSrc = /^(https:){0,1}\/\/.*?(youtube|vimeo|dailymotion|youku)/i;
- const allowedIframeSrcRegex = (function() {
- let reg = defaultAllowedIframeSrc;
- const SAFE_IFRAME_SRC_PATTERN =
- Meteor.settings.public.SAFE_IFRAME_SRC_PATTERN;
- try {
- if (SAFE_IFRAME_SRC_PATTERN !== undefined) {
- reg = new RegExp(SAFE_IFRAME_SRC_PATTERN, 'i');
- }
- } catch (e) {
- /*eslint no-console: ["error", { allow: ["warn", "error"] }] */
-
- console.error('Wrong pattern specified', SAFE_IFRAM_SRC_PATTERN, e);
- }
- return reg;
- })();
- const targetWindow = '_blank';
- const getHtmlDOM = html => {
- const i = document.createElement('i');
- i.innerHTML = html;
- return i.firstChild;
- };
- options = {
- onTag(tag, html, options) {
- const htmlDOM = getHtmlDOM(html);
- const getAttr = attr => {
- return htmlDOM && attr && htmlDOM.getAttribute(attr);
- };
- if (tag === 'iframe') {
- const clipCls = 'note-vide-clip';
- if (!options.isClosing) {
- const iframeCls = getAttr('class');
- let safe = iframeCls.indexOf(clipCls) > -1;
- const src = getAttr('src');
- if (allowedIframeSrcRegex.exec(src)) {
- safe = true;
- }
- if (safe)
- return `<iframe src='${src}' class="${clipCls}" width=100% height=auto allowfullscreen></iframe>`;
- } else {
- // remove </iframe> tag
- return '';
- }
- } else if (tag === 'a') {
- if (!options.isClosing) {
- if (getAttr(ASIS) === 'true') {
- // if has a ASIS attribute, don't do anything, it's a member id
- return html;
- } else {
- const href = getAttr('href');
- if (href.match(/^((http(s){0,1}:){0,1}\/\/|\/)/)) {
- // a valid url
- return `<a href=${href} target=${targetWindow}>`;
- }
- }
- }
- } else if (tag === 'img') {
- if (!options.isClosing) {
- const src = getAttr('src');
- if (src) {
- return `<a href='${src}' class='swipebox'><img src='${src}' class="attachment-image-preview mCS_img_loaded"></a>`;
- }
- }
- }
- return undefined;
- },
- onTagAttr(tag, name, value) {
- if (tag === 'img' && name === 'src') {
- if (value && value.substr(0, 5) === 'data:') {
- // allow image with dataURI src
- return `${name}='${value}'`;
- }
- } else if (tag === 'a' && name === 'target') {
- return `${name}='${targetWindow}'`; // always change a href target to a new window
- }
- return undefined;
- },
- ...options,
- };
- return _sanitizeXss(input, options);
-};
Template.editor.onRendered(() => {
const textareaSelector = 'textarea';
const mentions = [
@@ -94,13 +10,7 @@ Template.editor.onRendered(() => {
currentBoard
.activeMembers()
.map(member => {
- const user = Users.findOne(member.userId);
- if (user._id === Meteor.userId()) {
- return null;
- }
- const value = user.username;
- const username =
- value && value.match(/\s+/) ? `"${value}"` : value;
+ const username = Users.findOne(member.userId).username;
return username.includes(term) ? username : null;
})
.filter(Boolean),
@@ -120,16 +30,15 @@ Template.editor.onRendered(() => {
autosize($textarea);
$textarea.escapeableTextComplete(mentions);
};
- if (Meteor.settings.public.RICHER_CARD_COMMENT_EDITOR !== false) {
+ if (Meteor.settings.public.RICHER_CARD_COMMENT_EDITOR === 'true') {
const isSmall = Utils.isMiniScreen();
const toolbar = isSmall
? [
['view', ['fullscreen']],
['table', ['table']],
- ['font', ['bold']],
- ['color', ['color']],
- ['insert', ['video']], // iframe tag will be sanitized TODO if iframe[class=note-video-clip] can be added into safe list, insert video can be enabled
+ ['font', ['bold', 'underline']],
//['fontsize', ['fontsize']],
+ ['color', ['color']],
]
: [
['style', ['style']],
@@ -139,11 +48,47 @@ Template.editor.onRendered(() => {
['color', ['color']],
['para', ['ul', 'ol', 'paragraph']],
['table', ['table']],
- ['insert', ['link', 'picture', 'video']], // iframe tag will be sanitized TODO if iframe[class=note-video-clip] can be added into safe list, insert video can be enabled
+ //['insert', ['link', 'picture', 'video']], // iframe tag will be sanitized TODO if iframe[class=note-video-clip] can be added into safe list, insert video can be enabled
//['insert', ['link', 'picture']], // modal popup has issue somehow :(
['view', ['fullscreen', 'help']],
];
- const cleanPastedHTML = sanitizeXss;
+ const cleanPastedHTML = function(input) {
+ const badTags = [
+ 'style',
+ 'script',
+ 'applet',
+ 'embed',
+ 'noframes',
+ 'noscript',
+ 'meta',
+ 'link',
+ 'button',
+ 'form',
+ ].join('|');
+ const badPatterns = new RegExp(
+ `(?:${[
+ `<(${badTags})s*[^>][\\s\\S]*?<\\/\\1>`,
+ `<(${badTags})[^>]*?\\/>`,
+ ].join('|')})`,
+ 'gi',
+ );
+ let output = input;
+ // remove bad Tags
+ output = output.replace(badPatterns, '');
+ // remove attributes ' style="..."'
+ const badAttributes = new RegExp(
+ `(?:${[
+ 'on\\S+=([\'"]?).*?\\1',
+ 'href=([\'"]?)javascript:.*?\\2',
+ 'style=([\'"]?).*?\\3',
+ 'target=\\S+',
+ ].join('|')})`,
+ 'gi',
+ );
+ output = output.replace(badAttributes, '');
+ output = output.replace(/(<a )/gi, '$1target=_ '); // always to new target
+ return output;
+ };
const editor = '.editor';
const selectors = [
`.js-new-comment-form ${editor}`,
@@ -163,11 +108,27 @@ Template.editor.onRendered(() => {
}
return undefined;
};
+ // Prevent @member mentions on Add Comment input field
+ // from closing card, part 1.
let popupShown = false;
inputs.each(function(idx, input) {
mSummernotes[idx] = $(input).summernote({
placeholder,
+ // Prevent @member mentions on Add Comment input field
+ // from closing card, part 2.
+ onKeydown(e) {
+ if (popupShown) {
+ e.preventDefault();
+ }
+ },
+ onKeyup(e) {
+ if (popupShown) {
+ e.preventDefault();
+ }
+ },
callbacks: {
+ // Prevent @member mentions on Add Comment input field
+ // from closing card, part 3.
onKeydown(e) {
if (popupShown) {
e.preventDefault();
@@ -180,28 +141,19 @@ Template.editor.onRendered(() => {
},
onInit(object) {
const originalInput = this;
- const setAutocomplete = function(jEditor) {
- if (jEditor !== undefined) {
- jEditor.escapeableTextComplete(mentions).on({
- 'textComplete:show'() {
- popupShown = true;
- },
- 'textComplete:hide'() {
- popupShown = false;
- },
- });
- }
- };
- $(originalInput).on('submitted', function() {
- // resetCommentInput has been called
+ $(originalInput).on('input', function() {
+ // when comment is submitted, the original textarea will be set to '', so shall we
if (!this.value) {
const sn = getSummernote(this);
- sn && sn.summernote('code', '');
+ sn && sn.summernote('reset');
+ object && object.editingArea.find('.note-placeholder').show();
}
});
const jEditor = object && object.editable;
const toolbar = object && object.toolbar;
- setAutocomplete(jEditor);
+ if (jEditor !== undefined) {
+ jEditor.escapeableTextComplete(mentions);
+ }
if (toolbar !== undefined) {
const fBtn = toolbar.find('.btn-fullscreen');
fBtn.on('click', function() {
@@ -289,9 +241,15 @@ Template.editor.onRendered(() => {
const thisNote = this;
const updatePastedText = function(object) {
const someNote = getSummernote(object);
+ // Fix Pasting text into a card is adding a line before and after
+ // (and multiplies by pasting more) by changing paste "p" to "br".
+ // Fixes https://github.com/wekan/wekan/2890 .
+ // == Fix Start ==
+ someNote.execCommand('defaultParagraphSeparator', false, 'br');
+ // == Fix End ==
const original = someNote.summernote('code');
const cleaned = cleanPastedHTML(original); //this is where to call whatever clean function you want. I have mine in a different file, called CleanPastedHTML.
- someNote.summernote('code', ''); //clear original
+ someNote.summernote('reset'); //clear original
someNote.summernote('pasteHTML', cleaned); //this sets the displayed content editor to the cleaned pasted code.
};
setTimeout(function() {
@@ -331,6 +289,8 @@ Template.editor.onRendered(() => {
}
});
+import sanitizeXss from 'xss';
+
// XXX I believe we should compute a HTML rendered field on the server that
// would handle markdown and user mentions. We can simply have two
// fields, one source, and one compiled version (in HTML) and send only the
@@ -352,23 +312,28 @@ Blaze.Template.registerHelper(
}
return member;
});
- const mentionRegex = /\B@(?:(?:"([\w.\s]*)")|([\w.]+))/gi; // including space in username
+ const mentionRegex = /\B@([\w.]*)/gi;
let currentMention;
while ((currentMention = mentionRegex.exec(content)) !== null) {
- const [fullMention, quoteduser, simple] = currentMention;
- const username = quoteduser || simple;
+ const [fullMention, username] = currentMention;
const knowedUser = _.findWhere(knowedUsers, { username });
if (!knowedUser) {
continue;
}
const linkValue = [' ', at, knowedUser.username];
- let linkClass = 'atMention js-open-member';
+ //let linkClass = 'atMention js-open-member';
+ let linkClass = 'atMention';
if (knowedUser.userId === Meteor.userId()) {
linkClass += ' me';
}
- const link = HTML.A(
+ // This @user mention link generation did open same Wekan
+ // window in new tab, so now A is changed to U so it's
+ // underlined and there is no link popup. This way also
+ // text can be selected more easily.
+ //const link = HTML.A(
+ const link = HTML.U(
{
class: linkClass,
// XXX Hack. Since we stringify this render function result below with
@@ -376,42 +341,41 @@ Blaze.Template.registerHelper(
// `userId` to the popup as usual, and we need to store it in the DOM
// using a data attribute.
'data-userId': knowedUser.userId,
- [ASIS]: 'true',
},
linkValue,
);
content = content.replace(fullMention, Blaze.toHTML(link));
}
+
return HTML.Raw(sanitizeXss(content));
}),
);
+
Template.viewer.events({
// Viewer sometimes have click-able wrapper around them (for instance to edit
// the corresponding text). Clicking a link shouldn't fire these actions, stop
// we stop these event at the viewer component level.
'click a'(event, templateInstance) {
- let prevent = true;
+ event.stopPropagation();
+
+ // XXX We hijack the build-in browser action because we currently don't have
+ // `_blank` attributes in viewer links, and the transformer function is
+ // handled by a third party package that we can't configure easily. Fix that
+ // by using directly `_blank` attribute in the rendered HTML.
+ event.preventDefault();
+
const userId = event.currentTarget.dataset.userid;
if (userId) {
- Popup.open('member').call({ userId }, event, templateInstance);
+ // Prevent @member mentions on Add Comment input field
+ // from closing card, part 4.
+ PopupNoClose.open('member').call({ userId }, event, templateInstance);
+ event.preventDefault();
} else {
const href = event.currentTarget.href;
- const child = event.currentTarget.firstElementChild;
- if (child && child.tagName === 'IMG') {
- prevent = false;
- } else if (href) {
+ if (href) {
window.open(href, '_blank');
}
}
- if (prevent) {
- event.stopPropagation();
-
- // XXX We hijack the build-in browser action because we currently don't have
- // `_blank` attributes in viewer links, and the transformer function is
- // handled by a third party package that we can't configure easily. Fix that
- // by using directly `_blank` attribute in the rendered HTML.
- event.preventDefault();
- }
},
});
diff --git a/client/components/settings/peopleBody.jade b/client/components/settings/peopleBody.jade
index ca4bc382..fef1067e 100644
--- a/client/components/settings/peopleBody.jade
+++ b/client/components/settings/peopleBody.jade
@@ -110,7 +110,7 @@ template(name="editUserPopup")
label.hide.userId(type="text" value=user._id)
label
| {{_ 'fullname'}}
- input.js-profile-fullname(type="text" value=user.profile.fullname autofocus)
+ input.js-profile-fullname(type="text" value=user.profile.fullname)
label
| {{_ 'username'}}
span.error.hide.username-taken
@@ -159,7 +159,7 @@ template(name="newUserPopup")
//label.hide.userId(type="text" value=user._id)
label
| {{_ 'fullname'}}
- input.js-profile-fullname(type="text" value="" autofocus)
+ input.js-profile-fullname(type="text" value="")
label
| {{_ 'username'}}
span.error.hide.username-taken
diff --git a/client/components/sidebar/sidebar.jade b/client/components/sidebar/sidebar.jade
index ebcd8486..f0b0e4be 100644
--- a/client/components/sidebar/sidebar.jade
+++ b/client/components/sidebar/sidebar.jade
@@ -245,7 +245,7 @@ template(name="outgoingWebhooksPopup")
b &nbsp;
.materialCheckBox(class="{{#unless enabled}}is-checked{{/unless}}")
input.js-outgoing-webhooks-title(placeholder="{{_ 'webhook-title'}}" type="text" name="title" value=title)
- input.js-outgoing-webhooks-url(type="text" name="url" value=url autofocus)
+ input.js-outgoing-webhooks-url(type="text" name="url" value=url)
input.js-outgoing-webhooks-token(placeholder="{{_ 'webhook-token' }}" type="text" value=token name="token")
select.js-outgoing-webhooks-type(name="type")
each _type in types
@@ -257,7 +257,7 @@ template(name="outgoingWebhooksPopup")
input(type="hidden" value=_id name="id")
input.primary.wide(type="submit" value="{{_ 'save'}}")
form.integration-form
- input.js-outgoing-webhooks-title(placeholder="{{_ 'webhook-title'}}" type="text" name="title" autofocus)
+ input.js-outgoing-webhooks-title(placeholder="{{_ 'webhook-title'}}" type="text" name="title")
input.js-outgoing-webhooks-url(placeholder="{{_ 'URL' }}" type="text" name="url")
input.js-outgoing-webhooks-token(placeholder="{{_ 'webhook-token' }}" type="text" name="token")
select.js-outgoing-webhooks-type(name="type")
@@ -267,7 +267,10 @@ template(name="outgoingWebhooksPopup")
template(name="boardMenuPopup")
ul.pop-over-list
- li: a.js-custom-fields {{_ 'custom-fields'}}
+ li
+ a.js-custom-fields
+ i.fa.fa-list-alt
+ | {{_ 'custom-fields'}}
li
a.js-open-archives
i.fa.fa-archive
diff --git a/client/components/users/userHeader.jade b/client/components/users/userHeader.jade
index 9306d21d..1cd9da6b 100644
--- a/client/components/users/userHeader.jade
+++ b/client/components/users/userHeader.jade
@@ -98,12 +98,12 @@ template(name="changeLanguagePopup")
template(name="changeSettingsPopup")
ul.pop-over-list
- li
- a.js-toggle-system-messages
- i.fa.fa-comments-o
- | {{_ 'hide-system-messages'}}
- if hiddenSystemMessages
- i.fa.fa-check
+ //li
+ // a.js-toggle-system-messages
+ // i.fa.fa-comments-o
+ // | {{_ 'hide-system-messages'}}
+ // if hiddenSystemMessages
+ // i.fa.fa-check
li
a.js-toggle-desktop-drag-handles
i.fa.fa-arrows
diff --git a/client/lib/popup.js b/client/lib/popup.js
index 8095fbd2..8a55c2df 100644
--- a/client/lib/popup.js
+++ b/client/lib/popup.js
@@ -206,3 +206,207 @@ escapeActions.forEach(actionName => {
},
);
});
+
+// Prevent @member mentions on Add Comment input field
+// from closing card, part 5.
+// This duplicate below of above popup function is needed, because at
+// wekan/components/main/editor.js at bottom is popping up visible
+// @member mention, and it seems to trigger closing also card popup,
+// so in below closing popup is disabled.
+window.PopupNoClose = new (class {
+ constructor() {
+ // The template we use to render popups
+ this.template = Template.popup;
+
+ // We only want to display one popup at a time and we keep the view object
+ // in this `Popup.current` variable. If there is no popup currently opened
+ // the value is `null`.
+ this.current = null;
+
+ // It's possible to open a sub-popup B from a popup A. In that case we keep
+ // the data of popup A so we can return back to it. Every time we open a new
+ // popup the stack grows, every time we go back the stack decrease, and if
+ // we close the popup the stack is reseted to the empty stack [].
+ this._stack = [];
+
+ // We invalidate this internal dependency every time the top of the stack
+ // has changed and we want to re-render a popup with the new top-stack data.
+ this._dep = new Tracker.Dependency();
+ }
+
+ /// This function returns a callback that can be used in an event map:
+ /// Template.tplName.events({
+ /// 'click .elementClass': Popup.open("popupName"),
+ /// });
+ /// The popup inherit the data context of its parent.
+ open(name) {
+ const self = this;
+ const popupName = `${name}Popup`;
+ function clickFromPopup(evt) {
+ return $(evt.target).closest('.js-pop-over').length !== 0;
+ }
+ return function(evt) {
+ // If a popup is already opened, clicking again on the opener element
+ // should close it -- and interrupt the current `open` function.
+ /*
+ if (self.isOpen()) {
+ const previousOpenerElement = self._getTopStack().openerElement;
+ if (previousOpenerElement === evt.currentTarget) {
+ self.close();
+ return;
+ } else {
+ $(previousOpenerElement).removeClass('is-active');
+ }
+ }
+ */
+ // We determine the `openerElement` (the DOM element that is being clicked
+ // and the one we take in reference to position the popup) from the event
+ // if the popup has no parent, or from the parent `openerElement` if it
+ // has one. This allows us to position a sub-popup exactly at the same
+ // position than its parent.
+ let openerElement;
+ if (clickFromPopup(evt)) {
+ openerElement = self._getTopStack().openerElement;
+ } else {
+ self._stack = [];
+ openerElement = evt.currentTarget;
+ }
+ $(openerElement).addClass('is-active');
+ evt.preventDefault();
+
+ // We push our popup data to the stack. The top of the stack is always
+ // used as the data source for our current popup.
+ self._stack.push({
+ popupName,
+ openerElement,
+ hasPopupParent: clickFromPopup(evt),
+ title: self._getTitle(popupName),
+ depth: self._stack.length,
+ offset: self._getOffset(openerElement),
+ dataContext: (this && this.currentData && this.currentData()) || this,
+ });
+
+ // If there are no popup currently opened we use the Blaze API to render
+ // one into the DOM. We use a reactive function as the data parameter that
+ // return the complete along with its top element and depends on our
+ // internal dependency that is being invalidated every time the top
+ // element of the stack has changed and we want to update the popup.
+ //
+ // Otherwise if there is already a popup open we just need to invalidate
+ // our internal dependency, and since we just changed the top element of
+ // our internal stack, the popup will be updated with the new data.
+ if (!self.isOpen()) {
+ self.current = Blaze.renderWithData(
+ self.template,
+ () => {
+ self._dep.depend();
+ return { ...self._getTopStack(), stack: self._stack };
+ },
+ document.body,
+ );
+ } else {
+ self._dep.changed();
+ }
+ };
+ }
+
+ /// This function returns a callback that can be used in an event map:
+ /// Template.tplName.events({
+ /// 'click .elementClass': Popup.afterConfirm("popupName", function() {
+ /// // What to do after the user has confirmed the action
+ /// }),
+ /// });
+ afterConfirm(name, action) {
+ const self = this;
+
+ return function(evt, tpl) {
+ const context = (this.currentData && this.currentData()) || this;
+ context.__afterConfirmAction = action;
+ self.open(name).call(context, evt, tpl);
+ };
+ }
+
+ /// The public reactive state of the popup.
+ isOpen() {
+ this._dep.changed();
+ return Boolean(this.current);
+ }
+
+ /// In case the popup was opened from a parent popup we can get back to it
+ /// with this `Popup.back()` function. You can go back several steps at once
+ /// by providing a number to this function, e.g. `Popup.back(2)`. In this case
+ /// intermediate popup won't even be rendered on the DOM. If the number of
+ /// steps back is greater than the popup stack size, the popup will be closed.
+ back(n = 1) {
+ if (this._stack.length > n) {
+ _.times(n, () => this._stack.pop());
+ this._dep.changed();
+ }
+ // else {
+ // this.close();
+ //}
+ }
+
+ /// Close the current opened popup.
+ /*
+ close() {
+ if (this.isOpen()) {
+ Blaze.remove(this.current);
+ this.current = null;
+
+ const openerElement = this._getTopStack().openerElement;
+ $(openerElement).removeClass('is-active');
+
+ this._stack = [];
+ }
+ }
+ */
+
+ getOpenerComponent() {
+ const { openerElement } = Template.parentData(4);
+ return BlazeComponent.getComponentForElement(openerElement);
+ }
+
+ // An utility fonction that returns the top element of the internal stack
+ _getTopStack() {
+ return this._stack[this._stack.length - 1];
+ }
+
+ // We automatically calculate the popup offset from the reference element
+ // position and dimensions. We also reactively use the window dimensions to
+ // ensure that the popup is always visible on the screen.
+ _getOffset(element) {
+ const $element = $(element);
+ return () => {
+ Utils.windowResizeDep.depend();
+
+ if (Utils.isMiniScreen()) return { left: 0, top: 0 };
+
+ const offset = $element.offset();
+ const popupWidth = 300 + 15;
+ return {
+ left: Math.min(offset.left, $(window).width() - popupWidth),
+ top: offset.top + $element.outerHeight(),
+ };
+ };
+ }
+
+ // We get the title from the translation files. Instead of returning the
+ // result, we return a function that compute the result and since `TAPi18n.__`
+ // is a reactive data source, the title will be changed reactively.
+ _getTitle(popupName) {
+ return () => {
+ const translationKey = `${popupName}-title`;
+
+ // XXX There is no public API to check if there is an available
+ // translation for a given key. So we try to translate the key and if the
+ // translation output equals the key input we deduce that no translation
+ // was available and returns `false`. There is a (small) risk a false
+ // positives.
+ const title = TAPi18n.__(translationKey);
+ // when popup showed as full of small screen, we need a default header to clearly see [X] button
+ const defaultTitle = Utils.isMiniScreen() ? '' : false;
+ return title !== translationKey ? title : defaultTitle;
+ };
+ }
+})();