diff --git a/Bugzilla/Test/Selenium.pm b/Bugzilla/Test/Selenium.pm
index 4135ef06f2..31f5130dd9 100644
--- a/Bugzilla/Test/Selenium.pm
+++ b/Bugzilla/Test/Selenium.pm
@@ -426,6 +426,8 @@ sub is_editable_ok {
sub attach_file {
my ($self, $locator, $filename) = @_;
my $path = Mojo::File->new($filename);
+ # Click the Enter Text button to show the textarea for attachment data
+ $self->click_ok('att-enter-button');
$self->type_ok('att-textarea', $path->slurp, 'Add attachment data');
}
diff --git a/extensions/BugModal/template/en/default/bug_modal/field.html.tmpl b/extensions/BugModal/template/en/default/bug_modal/field.html.tmpl
index 0daebc5d93..823aa48285 100644
--- a/extensions/BugModal/template/en/default/bug_modal/field.html.tmpl
+++ b/extensions/BugModal/template/en/default/bug_modal/field.html.tmpl
@@ -152,7 +152,7 @@ END;
aria-required="true" aria-invalid="false" aria-errormessage="field-[% name FILTER html %]-error"
[% END %]
[% error_message = BLOCK %]
-
-[% WRAPPER product_block
- icon="component.png"
- onclick="product.select('Core', 'Untriaged')" %]
-HTML, CSS, JS, SVG, or some other web technology or combination of
-web technologies?
-[% END %]
-
-[% WRAPPER product_block
- icon="devedition.png"
- onclick="product.select('Firefox', 'Developer Tools')" %]
-Firefox's developer tools
-[% END %]
-
-[% WRAPPER product_block
- icon="firefox.png"
- onclick="product.select('Firefox', 'Untriaged')" %]
-Firefox's user interface (for example, an issue with bookmarks,
-tabbed browsing or the location bar)
-[% END %]
-
+
+
+
Select a product category:
+
+ [% WRAPPER product_block
+ icon="component.png"
+ product_name="Core"
+ component_name="Untriaged"
+ caption="Web Standards"
+ %]
+ Support for HTML, CSS, JavaScript, SVG, or any other web technology
+ [% END %]
+ [% WRAPPER product_block
+ icon="devedition.png"
+ product_name="DevTools"
+ caption="Firefox DevTools"
+ %]
+ Firefox browser’s built-in developer tools
+ [% END %]
+ [% WRAPPER product_block
+ icon="firefox.png"
+ product_name="Firefox"
+ component_name="Untriaged"
+ %]
+ Firefox browser’s user interface, including features like bookmarking,
+ tabbed browsing, location bar, etc.
+ [% END %]
+
+
[% END %]
@@ -121,65 +122,49 @@ tabbed browsing or the location bar)
[%############################################################################%]
[% BLOCK product_step %]
-
-
-[% INCLUDE page_title %]
-
-[% INCLUDE exits
- show = "all"
-%]
-
-
-[% INCLUDE 'guided/products.html.tmpl' %]
-
-
-
- [% PROCESS prodcompsearch/form.html.tmpl
- input_label = "Find product:"
- format = "guided"
- script_name = "enter_bug.cgi" %]
-
+
+ [% INCLUDE exits
+ show = "all"
+ %]
+
+
Select a product to enter a new [% terms.bug %]:
+
+ [% INCLUDE 'guided/products.html.tmpl' %]
+
+
[% END %]
[% BLOCK product_block %]
[% IF !caption %]
- [% caption = name %]
+ [% caption = product_name %]
[% END %]
[% IF !desc %]
[% FOREACH cls = classifications %]
[% FOREACH p = cls.products %]
- [% IF p.name == name %]
+ [% IF p.name == product_name %]
[% desc = p.description %]
[% LAST %]
[% END %]
[% END %]
[% END %]
[% END %]
-
`,
+ );
- _doSearch: async function() {
- if (dupes.getSummary().length < 4) {
- alert('The summary must be at least 4 characters long.');
return;
}
- dupes._elSummary.blur();
+
+ this.$summary.setAttribute('aria-invalid', 'false');
+ this.$summary.removeAttribute('aria-errormessage');
+ this.$summary.parentElement.querySelector('.error')?.remove();
+
+ this.$search.blur();
// don't query if we already have the results (or they are pending)
- if (dupes._currentSearchQuery == dupes.getSummary())
+ if (this.currentSearchQuery === this.summary) {
return;
- dupes._currentSearchQuery = dupes.getSummary();
+ }
+
+ this.currentSearchQuery = this.summary;
// initialize the datatable as late as possible
- dupes._initDataTable();
+ this.initDataTable();
try {
// run the search
- dupes._elList.classList.remove('hidden');
+ this.$list.hidden = false;
- dupes._dataTable.render([]);
- dupes._dataTable.setMessage(
- 'Searching for similar issues... ' +
- `'
+ const src = `${BUGZILLA.config.basepath}extensions/GuidedBugEntry/web/images/throbber.gif`;
+
+ this.dataTable.render([]);
+ this.dataTable.setMessage(
+ `Searching for similar issues... `,
);
- document.getElementById('dupes_continue_button_top').disabled = true;
- document.getElementById('dupes_continue_button_bottom').disabled = true;
- document.getElementById('dupes_continue').classList.remove('hidden');
+ document.querySelector('#dupe-continue-button').disabled = true;
+ document.querySelector('#dupe-continue').hidden = false;
let data;
+ const includeFields = [
+ 'id',
+ 'summary',
+ 'status',
+ 'resolution',
+ 'update_token',
+ 'cc',
+ 'component',
+ ];
+
try {
const { bugs } = await Bugzilla.API.get('bug/possible_duplicates', {
- product: product._getNameAndRelated(),
- summary: dupes.getSummary(),
+ product: GuidedBugEntryProductPage.productNameAndRelated,
+ summary: this.summary,
limit: 12,
- include_fields: ['id', 'summary', 'status', 'resolution', 'update_token', 'cc', 'component'],
+ include_fields: includeFields,
});
data = { results: bugs };
} catch (ex) {
- dupes._currentSearchQuery = '';
+ console.error(ex);
+ this.currentSearchQuery = '';
data = { error: true };
}
- document.getElementById('advanced').classList.remove('hidden');
- document.getElementById('dupes_continue_button_top').classList.remove('hidden');
- document.getElementById('dupes_continue_button_top').disabled = false;
- document.getElementById('dupes_continue_button_bottom').disabled = false;
- dupes._dataTable.update(data);
- } catch(err) {
- if (console)
- console.error(err.message);
- }
- },
-
- getSummary: function() {
- var summary = this._elSummary.value.trim();
- // work around chrome bug
- if (summary == dupes._elSummary.getAttribute('placeholder')) {
- return '';
- } else {
- return summary;
+ document.querySelector('#dupe-continue-button').disabled = false;
+ this.dataTable.update(data);
+ } catch (err) {
+ console.error(err.message);
}
}
-};
-// bug form step
+ /**
+ * Get the current summary value.
+ */
+ static get summary() {
+ return this.$summary.value.trim();
+ }
+}
+
+/**
+ * Bug entry form page.
+ */
+class GuidedBugEntryFormPage {
+ /**
+ * Reference to the bug form element.
+ * @type {HTMLFormElement}
+ */
+ static $form = null;
+
+ /**
+ * Reference to the submit button element.
+ * @type {HTMLInputElement}
+ */
+ static $submitButton = null;
+
+ /**
+ * Reference to the short description input element.
+ * @type {HTMLInputElement}
+ */
+ static $shortDescInput = null;
+
+ /**
+ * Reference to the hidden component input element.
+ * @type {HTMLInputElement}
+ */
+ static $component = null;
+
+ /**
+ * Reference to the component select element.
+ * @type {HTMLSelectElement}
+ */
+ static $componentSelect = null;
+
+ /**
+ * Reference to the component description element.
+ * @type {HTMLDivElement}
+ */
+ static $componentDesc = null;
+
+ /**
+ * Reference to the hidden version input element.
+ * @type {HTMLInputElement}
+ */
+ static $version = null;
+
+ /**
+ * Reference to the version select element.
+ * @type {HTMLSelectElement}
+ */
+ static $versionSelect = null;
+
+ /**
+ * Reference to the currently visible help panel.
+ * @type {HTMLElement}
+ */
+ static $visibleHelpPanel = null;
+
+ /**
+ * List of elements that are conditionally displayed.
+ * @type {{ check: () => boolean, id: string }[]}
+ */
+ static conditionalDetails = [
+ {
+ check: () => GuidedBugEntryProductPage.productName === 'Firefox',
+ id: 'firefox-android-row',
+ },
+ ];
+
+ /**
+ * Maximum attachment size in KB.
+ * @type {number}
+ */
+ static maxAttachmentSize = Number(BUGZILLA.param.maxattachmentsize);
+
+ /**
+ * Initialization callback.
+ */
+ static onInit() {
+ this.$form = document.querySelector('#bug-form');
+ this.$submitButton = document.querySelector('#submit-button');
+ this.$shortDescInput = document.querySelector('#short-desc');
+ this.$component = document.querySelector('#component');
+ this.$componentSelect = document.querySelector('#component-select');
+ this.$componentDesc = document.querySelector('#component-description');
+ this.$version = document.querySelector('#version');
+ this.$versionSelect = document.querySelector('#version-select');
+ this.$attPlaceholder = document.querySelector('#att-placeholder');
+ this.$attDescSection = document.querySelector('#att-desc-section');
+ this.$attDescription = document.querySelector('#att-description');
+ this.$attMimeType = document.querySelector('[name="contenttypeentry"]');
+ this.$attIsPatch = document.querySelector('[name="ispatch"]');
+
+ document.querySelector('#user_agent').value = navigator.userAgent;
+
+ this.$shortDescInput.addEventListener('blur', () => {
+ document.querySelector('#dupe-summary').value = this.$shortDescInput.value;
+ GuidedBugEntry.setAdvancedLink();
+ });
+
+ this.$form.addEventListener('submit', async (event) => this.submitForm(event));
+
+ this.$versionSelect.addEventListener('change', (event) => {
+ this.onVersionChange(event.target.value);
+ });
+
+ this.$componentSelect.addEventListener('change', (event) => {
+ this.onComponentChange(event.target.value);
+ });
+
+ this.$attDescription.addEventListener('change', () => {
+ this.attDescOverridden = true;
+ });
-var bugForm = {
- _visibleHelpPanel: null,
- _mandatoryFields: [],
- _conditionalDetails: [
- { check: function () { return product.getName() == 'Firefox'; }, id: 'firefox_for_android_row' }
- ],
+ const useMarkdown = BUGZILLA.param.use_markdown;
- onInit: function() {
- var user_agent = navigator.userAgent;
- document.getElementById('user_agent').value = navigator.userAgent;
- document.getElementById('short_desc').addEventListener('blur', () => {
- document.getElementById('dupes_summary').value = document.getElementById('short_desc').value;
- guided.setAdvancedLink();
+ document.querySelectorAll('#bug-form textarea.description').forEach(($textarea) => {
+ new Bugzilla.CommentEditor({ $textarea, useMarkdown, showTips: false }).render();
});
- },
+ }
+
+ /**
+ * Show help tooltip.
+ * @param {HTMLElement} $tooltip Tooltip element.
+ * @param {HTMLElement} $icon Help icon element.
+ */
+ static showHelp($tooltip, $icon) {
+ if (this.$visibleHelpPanel) {
+ this.$visibleHelpPanel.hidden = true;
+ }
+
+ const { top, left } = $icon.getBoundingClientRect();
+
+ $tooltip.style.inset = `${top + 24}px auto auto ${left - 320}px`;
+ $tooltip.hidden = false;
+ this.$visibleHelpPanel = $tooltip;
+ }
- initHelp: function() {
- document.querySelectorAll('.help_icon').forEach(($icon) => {
+ /**
+ * Hide help tooltip.
+ * @param {HTMLElement} $tooltip Tooltip element.
+ */
+ static hideHelp($tooltip) {
+ $tooltip.hidden = true;
+ this.$visibleHelpPanel = null;
+ }
+
+ /**
+ * Initialize help tooltips.
+ */
+ static initHelp() {
+ document.querySelectorAll('.help-trigger').forEach(($icon) => {
const $tooltip = document.getElementById($icon.getAttribute('aria-describedby'));
$icon.addEventListener('mouseover', () => {
- if (this._visibleHelpPanel) {
- this._visibleHelpPanel.hidden = true;
- }
+ this.showHelp($tooltip, $icon);
+ });
- const { top, left } = $icon.getBoundingClientRect();
+ $icon.addEventListener('mouseout', () => {
+ this.hideHelp($tooltip);
+ });
- $tooltip.style.inset = `${top}px auto auto ${left + 24}px`;
- $tooltip.hidden = false;
- this._visibleHelpPanel = $tooltip;
+ $icon.addEventListener('focus', () => {
+ this.showHelp($tooltip, $icon);
});
- $icon.addEventListener('mouseout', () => {
- $tooltip.hidden = true;
- this._visibleHelpPanel = null;
+ $icon.addEventListener('blur', () => {
+ this.hideHelp($tooltip);
+ });
+
+ $icon.addEventListener('click', (event) => {
+ event.preventDefault();
});
});
- },
+ }
- onShow: function() {
+ /**
+ * Show callback.
+ */
+ static onShow() {
// check for a forced format
- var productName = product.getName();
- var visibleCount = 0;
- if (products[productName] && products[productName].format) {
- document.getElementById('advanced').classList.add('hidden');
- document.location.href = `${BUGZILLA.config.basepath}enter_bug.cgi?` +
- `format=${encodeURIComponent(products[productName].format)}` +
- `&product=${encodeURIComponent(productName)}` +
- `&short_desc=${encodeURIComponent(dupes.getSummary())}`;
- guided.updateStep = false;
- return;
+ const productName = GuidedBugEntryProductPage.productName;
+ const productSummary = GuidedBugEntryOtherDupesPage.summary;
+ const { format } = products[productName] ?? {};
+
+ if (format) {
+ const params = new URLSearchParams({
+ format,
+ product: productName,
+ short_desc: productSummary,
+ });
+
+ document.location.href = `${BUGZILLA.config.basepath}enter_bug.cgi?${params}`;
+ GuidedBugEntry.updateStep = false;
+
+ return;
}
- document.getElementById('advanced').classList.remove('hidden');
+
// default the summary to the dupes query
- document.getElementById('short_desc').value = dupes.getSummary();
+ this.$shortDescInput.value = productSummary;
this.resetSubmitButton();
- if (document.getElementById('component_select').length == 0)
+
+ if (this.$componentSelect.length === 0) {
this.onProductUpdated();
- this.onFileChange();
- this._mandatoryFields.forEach((id) => {
- document.getElementById(id).classList.remove('missing');
+ }
+
+ new Bugzilla.AttachmentSelector({
+ $placeholder: this.$attPlaceholder,
+ eventHandlers: {
+ AttachmentProcessed: (event) => this.onAttachmentProcessed(event),
+ AttachmentTextUpdated: (event) => this.onAttachmentTextUpdated(event),
+ },
});
- this._conditionalDetails.forEach(function (cond) {
- if (cond.check()) {
- visibleCount++;
- document.getElementById(cond.id).classList.remove('hidden');
- }
- else {
- document.getElementById(cond.id).classList.add('hidden');
- }
+ this.requiredFields.forEach((el) => {
+ el.removeAttribute('aria-invalid');
+ el.removeAttribute('aria-errormessage');
+ el.parentElement.querySelector('.error')?.remove();
+ });
+
+ this.conditionalDetails.forEach((cond) => {
+ document.getElementById(cond.id).hidden = !cond.check();
+ });
+
+ window.requestAnimationFrame(() => {
+ this.$shortDescInput.focus();
+ this.$shortDescInput.select();
});
- if (visibleCount > 0) {
- document.getElementById('details').classList.remove('hidden');
- document.getElementById('submitTR').classList.remove('even');
+
+ GuidedBugEntry.updateSteppers('form');
+ }
+
+ /**
+ * Called whenever a file is processed by `AttachmentSelector`. Update the Description, Content
+ * Type, Patch checkbox, etc. based on the file properties and content.
+ * @param {object} params An object with the following properties:
+ * @param {File} params.file A processed `File` object.
+ * @param {string} params.type A MIME type to be selected in the Content Type field.
+ * @param {boolean} params.isPatch `true` if the file is detected as a patch, `false` otherwise.
+ */
+ static onAttachmentProcessed({ file, type, isPatch }) {
+ this.$attDescSection.hidden = false;
+ this.$attDescription.value = file.name;
+ this.$attDescription.disabled = false;
+ this.$attDescription.setAttribute('aria-required', true);
+ this.$attMimeType.value = type;
+ this.$attIsPatch.value = isPatch ? 'on' : '';
+ }
+
+ /**
+ * Called whenever the attachment text is updated. Update the Content Type, Patch checkbox, etc.
+ * based on the new content.
+ * @param {object} params An object with the following properties:
+ * @param {string} params.text The new text content.
+ * @param {boolean} params.hasText `true` if the textarea has non-empty content, `false`
+ * otherwise.
+ * @param {boolean} params.isPatch `true` if the content is detected as a patch, `false`
+ * otherwise.
+ * @param {boolean} params.isGhpr `true` if the content is detected as a GitHub Pull Request link,
+ * `false` otherwise.
+ */
+ static onAttachmentTextUpdated({ text, hasText, isPatch, isGhpr }) {
+ if (!this.attDescOverridden) {
+ this.$attDescription.value = isPatch ? 'patch' : isGhpr ? 'GitHub Pull Request' : '';
}
- else {
- document.getElementById('details').classList.add('hidden');
- document.getElementById('submitTR').classList.add('even');
+
+ this.$attDescSection.hidden = !hasText;
+ this.$attDescription.disabled = !hasText;
+ this.$attDescription.setAttribute('aria-required', hasText);
+ this.$attMimeType.value = isGhpr ? 'text/x-github-pull-request' : 'text/plain';
+ this.$attIsPatch.value = isPatch ? 'on' : '';
+
+ if (!hasText) {
+ this.attDescOverridden = false;
}
- },
+ }
- resetSubmitButton: function() {
- document.getElementById('submit').disabled = false;
- document.getElementById('submit').value = 'Submit Bug';
- },
+ /**
+ * Reset the submit button state.
+ */
+ static resetSubmitButton() {
+ this.$submitButton.disabled = false;
+ this.$submitButton.value = 'Submit Bug';
+ }
- onProductUpdated: function() {
- var productName = product.getName();
+ /**
+ * Escape special characters for use in a regex.
+ * @param {string} value Input string.
+ * @returns {string} Escaped string.
+ */
+ static quoteMeta(value) {
+ return value.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
+ }
+
+ /**
+ * Product updated callback.
+ */
+ static onProductUpdated() {
+ const productName = GuidedBugEntryProductPage.productName;
// init
- var elComponents = document.getElementById('component_select');
- document.getElementById('component_description').classList.add('hidden');
- elComponents.options.length = 0;
+ const $components = this.$componentSelect;
+ const $versions = this.$versionSelect;
- var elVersions = document.getElementById('version_select');
- elVersions.length = 0;
+ this.$componentDesc.hidden = true;
+ $components.options.length = 0;
+ $versions.options.length = 0;
// product not loaded yet, bail out
- if (!product.details) {
- document.getElementById('versionTD').classList.add('hidden');
- document.getElementById('productTD').colSpan = 2;
- document.getElementById('submit').disabled = true;
+ if (!GuidedBugEntryProductPage.details) {
+ this.$submitButton.disabled = true;
+
return;
}
- document.getElementById('submit').disabled = false;
+
+ this.$submitButton.disabled = false;
+
+ const { componentFilter, noComponentSelection, defaultComponent } = products[productName] ?? {};
// filter components
- if (products[productName] && products[productName].componentFilter) {
- product.details.components = products[productName].componentFilter(product.details.components);
+ if (componentFilter) {
+ GuidedBugEntryProductPage.details.components = componentFilter(
+ GuidedBugEntryProductPage.details.components,
+ );
}
// build components
-
- var elComponent = document.getElementById('component');
- if (products[productName] && products[productName].noComponentSelection || guided.webdev) {
- elComponent.value = products[productName].defaultComponent;
- bugForm._mandatoryFields = [ 'short_desc', 'version_select' ];
+ if (noComponentSelection || GuidedBugEntry.webdev) {
+ this.$component.value = defaultComponent;
+ this.$componentSelect.removeAttribute('aria-required');
} else {
- bugForm._mandatoryFields = [ 'short_desc', 'component_select', 'version_select' ];
+ this.$componentSelect.setAttribute('aria-required', 'true');
+
+ const { preselectedComponent } = GuidedBugEntryProductPage;
// check for the default component
- var defaultRegex;
- if (product.getPreselectedComponent()) {
- defaultRegex = new RegExp('^' + quoteMeta(product.getPreselectedComponent()) + '$', 'i');
- } else if(products[productName] && products[productName].defaultComponent) {
- defaultRegex = new RegExp('^' + quoteMeta(products[productName].defaultComponent) + '$', 'i');
- } else {
- defaultRegex = new RegExp('General', 'i');
- }
+ const defaultRegex = preselectedComponent
+ ? new RegExp(`^${this.quoteMeta(preselectedComponent)}$`, 'i')
+ : defaultComponent
+ ? new RegExp(`^${this.quoteMeta(defaultComponent)}$`, 'i')
+ : new RegExp('General', 'i');
- var preselectedComponent = false;
- var i, n;
- var component;
- for (i = 0, n = product.details.components.length; i < n; i++) {
- component = product.details.components[i];
- if (component.is_active == '1') {
- if (defaultRegex.test(component.name)) {
- preselectedComponent = component.name;
- break;
- }
- }
- }
+ const { components } = GuidedBugEntryProductPage.details;
+ const component = components.find((c) => c.is_active && defaultRegex.test(c.name));
+ const preselectedComponentName = component?.name ?? null;
// if there isn't a default component, default to blank
- if (!preselectedComponent) {
- elComponents.options[elComponents.options.length] = new Option('', '');
+ if (!preselectedComponentName) {
+ $components.options.add(new Option('', ''));
}
// build component select
- for (i = 0, n = product.details.components.length; i < n; i++) {
- component = product.details.components[i];
- if (component.is_active == '1') {
- elComponents.options[elComponents.options.length] =
- new Option(component.name, component.name);
+ components.forEach((c) => {
+ if (c.is_active) {
+ $components.options.add(new Option(c.name, c.name));
}
- }
+ });
- var validComponent = false;
- for (i = 0, n = elComponents.options.length; i < n && !validComponent; i++) {
- if (elComponents.options[i].value == elComponent.value)
- validComponent = true;
+ const validComponent = [...$components.options].some(
+ (o) => o.value === this.$component.value,
+ );
+
+ if (!validComponent) {
+ this.$component.value = '';
}
- if (!validComponent)
- elComponent.value = '';
- if (elComponent.value == '' && preselectedComponent)
- elComponent.value = preselectedComponent;
- if (elComponent.value != '') {
- elComponents.value = elComponent.value;
- this.onComponentChange(elComponent.value);
+
+ if (this.$component.value === '' && preselectedComponentName) {
+ this.$component.value = preselectedComponentName;
}
+ if (this.$component.value !== '') {
+ $components.value = this.$component.value;
+ this.onComponentChange(this.$component.value);
+ }
}
// build versions
- var defaultVersion = '';
- var currentVersion = document.getElementById('version').value;
- for (i = 0, n = product.details.versions.length; i < n; i++) {
- var version = product.details.versions[i];
- if (version.is_active == '1') {
- elVersions.options[elVersions.options.length] =
- new Option(version.name, version.name);
- if (currentVersion == version.name)
- defaultVersion = version.name;
+ const currentVersion = this.$version.value;
+ let defaultVersion = '';
+
+ GuidedBugEntryProductPage.details.versions.forEach(({ is_active, name }) => {
+ if (is_active) {
+ $versions.options.add(new Option(name, name));
+
+ if (currentVersion === name) {
+ defaultVersion = name;
+ }
}
- }
+ });
if (!defaultVersion) {
// try to detect version on a per-product basis
- if (products[productName] && products[productName].version) {
- var detectedVersion = products[productName].version();
- var options = elVersions.options;
- for (i = 0, n = options.length; i < n; i++) {
- if (options[i].value == detectedVersion) {
- defaultVersion = detectedVersion;
- break;
- }
+ if (products[productName]?.version) {
+ const detectedVersion = products[productName].version();
+
+ if ([...$versions.options].some((o) => o.value === detectedVersion)) {
+ defaultVersion = detectedVersion;
}
}
}
- if (elVersions.length > 1) {
+ if ($versions.length > 1) {
// more than one version, show select
- document.getElementById('productTD').colSpan = 1;
- document.getElementById('versionTD').classList.remove('hidden');
-
+ document.querySelector('#version-section').hidden = false;
} else {
// if there's only one version, we don't need to ask the user
- document.getElementById('versionTD').classList.add('hidden');
- document.getElementById('productTD').colSpan = 2;
- defaultVersion = elVersions.options[0].value;
+ document.querySelector('#version-section').hidden = true;
+ defaultVersion = $versions.options[0]?.value;
}
if (defaultVersion) {
- elVersions.value = defaultVersion;
-
+ $versions.value = defaultVersion;
} else {
- // no default version, select an empty value to force a decision
- var opt = new Option('', '');
- try {
- // standards
- elVersions.add(opt, elVersions.options[0]);
- } catch(ex) {
- // IE only
- elVersions.add(opt, 0);
+ // Fallback to 'unspecified' if available
+ const index = [...$versions.options].findIndex((o) => o.value === 'unspecified');
+
+ if (index > -1) {
+ $versions.value = 'unspecified';
}
- elVersions.value = '';
}
- bugForm.onVersionChange(elVersions.value);
+
+ this.onVersionChange($versions.value);
// Set default Platform, OS and Security Group
// Skip if the default value is empty = auto-detect
- const { default_platform, default_op_sys, default_security_group } = product.details;
+ const { default_platform, default_op_sys, default_security_group } =
+ GuidedBugEntryProductPage.details;
if (default_platform) {
document.querySelector('#rep_platform').value = default_platform;
@@ -773,106 +1233,115 @@ var bugForm = {
if (default_security_group) {
document.querySelector('#groups').value = default_security_group;
}
- },
-
- onComponentChange: function(componentName) {
- // show the component description
- document.getElementById('component').value = componentName;
- var elComponentDesc = document.getElementById('component_description');
- elComponentDesc.innerHTML = '';
- for (var i = 0, n = product.details.components.length; i < n; i++) {
- var component = product.details.components[i];
- if (component.name == componentName) {
- elComponentDesc.innerHTML = component.description;
- break;
- }
- }
- elComponentDesc.classList.remove('hidden');
- },
-
- onVersionChange: function(version) {
- document.getElementById('version').value = version;
- },
-
- onFileChange: function() {
- // toggle ui enabled when a file is uploaded or cleared
- var elFile = document.getElementById('data');
- var elReset = document.getElementById('reset_data');
- var elDescription = document.getElementById('data_description');
- var filename = bugForm._getFilename();
- elReset.disabled = !filename;
- elDescription.value = filename || '';
- elDescription.disabled = !filename;
- document.getElementById('reset_data').classList.toggle('hidden', !filename);
- document.getElementById('data_description_tr').classList.toggle('hidden', !filename);
- },
-
- onFileClear: function() {
- document.getElementById('data').value = '';
- this.onFileChange();
- return false;
- },
-
- _getFilename: function() {
- var filename = document.getElementById('data').value;
- if (!filename)
- return '';
- filename = filename.replace(/^.+[\\\/]/, '');
- return filename;
- },
-
- _mandatoryMissing: function() {
- var result = new Array();
- for (var i = 0, n = this._mandatoryFields.length; i < n; i++ ) {
- var id = this._mandatoryFields[i];
- var el = document.getElementById(id);
- var value;
-
- if (el.type.toString() == "checkbox") {
- value = el.checked;
+ }
+
+ /**
+ * Component change handler. Sets the hidden component input and shows the description.
+ * @param {string} componentName Component name.
+ */
+ static onComponentChange(componentName) {
+ this.$component.value = componentName;
+
+ const component = GuidedBugEntryProductPage.details.components.find(
+ (c) => c.name === componentName,
+ );
+
+ this.$componentDesc.innerHTML = component?.description ?? '';
+ this.$componentDesc.hidden = false;
+ }
+
+ /**
+ * Version change handler. Sets the hidden version input.
+ * @param {string} version Version name.
+ */
+ static onVersionChange(version) {
+ this.$version.value = version;
+ }
+
+ /**
+ * Get all required fields in the form.
+ * @returns {HTMLElement[]} Array of required field elements.
+ */
+ static get requiredFields() {
+ return [...this.$form.querySelectorAll('[aria-required="true"]:not([aria-hidden="true"])')];
+ }
+
+ /**
+ * Check for missing required fields.
+ * @returns {string[]} Array of IDs of missing required fields.
+ */
+ static checkMissing() {
+ /** @type {string[]} */
+ const result = [];
+
+ this.requiredFields.forEach(($field) => {
+ let invalid = false;
+ let message = '';
+
+ if ($field.getAttribute('role') === 'radiogroup') {
+ invalid = ![...$field.querySelectorAll('input')].some((r) => r.checked);
+ message = 'Please select an option.';
+ } else if ($field.matches('select')) {
+ invalid = !$field.value.trim();
+ message = 'Please select an option.';
} else {
- value = el.value.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
- el.value = value;
+ invalid = !$field.value.trim();
+ message = $field.matches('[data-allow-na]')
+ ? 'This field is required. Write “N/A” if not applicable.'
+ : 'This field is required.';
}
- if (value == '') {
- document.getElementById(id).classList.add('missing');
- result.push(id);
+ $field.setAttribute('aria-invalid', invalid);
+ $field.setAttribute('aria-errormessage', invalid ? `${$field.id}-error` : '');
+
+ if (invalid) {
+ result.push($field.id);
+
+ if (!$field.parentElement.querySelector('.error-message')) {
+ $field.insertAdjacentHTML(
+ 'afterend',
+ `
${message}
`,
+ );
+ }
} else {
- document.getElementById(id).classList.remove('missing');
+ $field.parentElement.querySelector('.error-message')?.remove();
}
- }
- return result;
- },
+ });
- validate: function() {
+ return result;
+ }
- // check mandatory fields
+ /**
+ * Validate the form before submission.
+ * @returns {boolean} Whether the form is valid for submission.
+ */
+ static validate() {
+ const missing = this.checkMissing();
- var missing = bugForm._mandatoryMissing();
if (missing.length) {
- var message = 'The following field' +
- (missing.length == 1 ? ' is' : 's are') + ' required:\n\n';
- for (var i = 0, n = missing.length; i < n; i++ ) {
- var id = missing[i];
- if (id == 'short_desc') message += ' Summary\n';
- if (id == 'component_select') message += ' Component\n';
- if (id == 'version_select') message += ' Version\n';
- }
- alert(message);
+ document.getElementById(missing[0])?.focus();
return false;
}
- if (document.getElementById('data').value && !document.getElementById('data_description').value)
- document.getElementById('data_description').value = bugForm._getFilename();
+ return true;
+ }
+
+ /**
+ * Submit the bug form.
+ */
+ static async submitForm(event) {
+ if (!this.validate()) {
+ event.preventDefault();
+ return false;
+ }
- document.getElementById('submit').disabled = true;
- document.getElementById('submit').value = 'Submitting Bug...';
+ this.$submitButton.disabled = true;
+ this.$submitButton.value = 'Submitting Bug...';
return true;
- },
-};
-
-function quoteMeta(value) {
- return value.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
+ }
}
+
+window.addEventListener('DOMContentLoaded', () => {
+ GuidedBugEntry.init();
+});
diff --git a/extensions/GuidedBugEntry/web/js/products.js b/extensions/GuidedBugEntry/web/js/products.js
index 2f1ccb8b7e..c3da57655e 100644
--- a/extensions/GuidedBugEntry/web/js/products.js
+++ b/extensions/GuidedBugEntry/web/js/products.js
@@ -9,128 +9,100 @@
*
* related: array of product names which will also be searched for duplicates
* version: function which returns a version (e.g. detected from UserAgent)
- * support: string which is displayed at the top of the duplicates page
- * secgroup: the group to place confidential bugs into
* defaultComponent: the default component to select. Defaults to 'General'
* noComponentSelection: when true, the default component will always be
* used. Defaults to 'false';
- * detectPlatform: when true the platform and op_sys will be set from the
- * browser's user agent. when false, these will be set to All
+ * l10n: allow selecting Localization product
+ * support: string which is displayed at the top of the duplicates page
*/
-var products = {
- "addons.mozilla.org": {
- l10n: true
+const products = {
+ 'addons.mozilla.org': {
+ l10n: true,
},
- "Firefox": {
- related: [ "Core", "Toolkit" ],
- version: function() {
- var re = /Firefox\/(\d+)\.(\d+)/i;
- var match = re.exec(navigator.userAgent);
- if (match) {
- var maj = match[1];
- var min = match[2];
- if (maj * 1 >= 80) {
- return "Firefox " + maj;
- } else if (maj * 1 >= 5) {
- return maj + " Branch";
+ Firefox: {
+ related: ['Core', 'Toolkit'],
+ version: () => {
+ const re = /Firefox\/(?\d+)\.(?\d+)/i;
+ const groups = navigator.userAgent.match(re)?.groups;
+ if (groups) {
+ const { major, minor } = groups;
+ if (major * 1 >= 80) {
+ return 'Firefox ' + major;
+ } else if (major * 1 >= 5) {
+ return `${major} Branch`;
} else {
- return maj + "." + min + " Branch";
+ return `${major}.${minor} Branch`;
}
- } else {
- return false;
}
+ return false;
},
- defaultComponent: "Untriaged",
+ defaultComponent: 'Untriaged',
noComponentSelection: true,
- detectPlatform: true,
l10n: true,
support:
'If you are new to Firefox or Bugzilla, please consider checking ' +
- '' +
- `` +
- ' Firefox Help instead of creating a bug.'
+ '' +
+ 'Firefox Support instead of creating a bug.',
},
- "Firefox for Android": {
- related: [ "Core", "Toolkit" ],
- detectPlatform: true,
+ 'Firefox for Android': {
+ related: ['Core', 'Toolkit'],
l10n: true,
support:
'If you are new to Firefox or Bugzilla, please consider checking ' +
- '' +
- `` +
- ' Firefox Help instead of creating a bug.'
+ '' +
+ 'Firefox for Android Support instead of creating a bug.',
},
- "SeaMonkey": {
- related: [ "Core", "Toolkit", "MailNews Core" ],
- detectPlatform: true,
+ Focus: {
l10n: true,
- version: function() {
- var re = /SeaMonkey\/(\d+)\.(\d+)/i;
- var match = re.exec(navigator.userAgent);
- if (match) {
- var maj = match[1];
- var min = match[2];
- return "SeaMonkey " + maj + "." + min + " Branch";
- } else {
- return false;
- }
- }
},
- "Calendar": {
- l10n: true
- },
-
- "Camino": {
- related: [ "Core", "Toolkit" ],
- detectPlatform: true
+ SeaMonkey: {
+ related: ['Core', 'Toolkit', 'MailNews Core'],
+ l10n: true,
+ version: () => {
+ const re = /SeaMonkey\/(?\d+)\.(?\d+)/i;
+ const groups = navigator.userAgent.match(re)?.groups;
+ if (groups) {
+ const { major, minor } = groups;
+ return `SeaMonkey ${major}.${minor} Branch`;
+ }
+ return false;
+ },
},
- "Core": {
- detectPlatform: true
+ Calendar: {
+ l10n: true,
},
- "Thunderbird": {
- related: [ "Core", "Toolkit", "MailNews Core" ],
- detectPlatform: true,
+ Thunderbird: {
+ related: ['Core', 'Toolkit', 'MailNews Core'],
l10n: true,
- defaultComponent: "Untriaged",
- componentFilter : function(components) {
- var index = -1;
- for (var i = 0, l = components.length; i < l; i++) {
- if (components[i].name == 'General') {
- index = i;
- break;
- }
- }
- if (index != -1) {
- components.splice(index, 1);
- }
- return components;
+ defaultComponent: 'Untriaged',
+ componentFilter: (components) => {
+ const index = components.findIndex((c) => c.name === 'General');
+ if (index !== -1) {
+ components.splice(index, 1);
+ }
+ return components;
},
support:
'If you are new to Thunderbird or Bugzilla, please consider checking ' +
- '' +
- `` +
- ' Thunderbird Help instead of creating a bug.'
- },
-
- "Penelope": {
- related: [ "Core", "Toolkit", "MailNews Core" ]
+ '' +
+ 'Thunderbird Support instead of creating a bug.',
},
- "Bugzilla": {
+ Bugzilla: {
support:
- 'Please use our test server to file "test bugs".'
+ 'Please use our test server to file "test bugs".',
},
- "bugzilla.mozilla.org": {
- related: [ "Bugzilla" ],
+ 'bugzilla.mozilla.org': {
+ related: ['Bugzilla'],
support:
- 'Please use our test server to file "test bugs".'
- }
+ 'Please use our test server to file "test bugs".',
+ },
};
diff --git a/extensions/GuidedBugEntry/web/style/guided.css b/extensions/GuidedBugEntry/web/style/guided.css
index 1c615fd805..215d21afec 100644
--- a/extensions/GuidedBugEntry/web/style/guided.css
+++ b/extensions/GuidedBugEntry/web/style/guided.css
@@ -9,54 +9,209 @@
* global
*/
-#page_title h2 {
- margin-bottom: 0;
+#bugzilla-body:has(#guided) {
+ display: flex;
+ flex-direction: column;
+ min-height: calc(100dvh - var(--global-header-height));
}
-#page_title h3 {
- margin-top: 0;
+#main-inner:has(#guided) {
+ flex: auto;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+}
+
+#guided {
+ flex: auto;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ width: 100%;
+}
+
+#guided > * {
+ width: 100%;
+}
+
+#guided :is([role="region"], [role="search"]) {
+ border: 1px solid var(--primary-region-border-color);
+ border-radius: var(--primary-region-border-radius);
+ padding: 16px;
+ background-color: var(--primary-region-background-color);
+ box-shadow: var(--primary-region-box-shadow);
+}
+
+#guided button.iconic {
+ border: none !important;
+ padding: 0 !important;
+ width: 24px;
+ height: 24px;
+ color: var(--secondary-label-color) !important;
+ background-color: transparent !important;
+ line-height: 1 !important;
+}
+
+#guided .icon {
+ font-size: var(--icon-size-medium);
+ line-height: 1;
+ font-family: var(--icon-font-family);
+ font-variation-settings:
+ "FILL" 0,
+ "wght" 300,
+ "GRAD" 0,
+ "opsz" 24;
+}
+
+#guided .messages {
+ margin: 0;
+ border: 1px solid var(--control-border-color);
+ border-radius: 8px;
+ padding: 12px;
+ background-color: var(--control-background-color);
}
-.hidden {
+#guided .messages:has(#product-support[hidden]):has(#l10n-message[hidden]) {
display: none;
}
+#page-header {
+ flex: none;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ box-sizing: border-box;
+ margin: 0 auto;
+ border-bottom: 1px solid var(--control-border-color);
+ padding: 0 0 16px;
+ max-width: 1200px;
+}
+
+#page-header h1 {
+ margin: 0;
+ text-align: center;
+}
+
+#page-header h3 {
+ margin-top: 0;
+}
+
+#stepper {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 16px;
+ margin: 0;
+ padding: 0;
+}
+
+#stepper div {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 8px;
+ width: 120px;
+ color: var(--secondary-label-color);
+}
+
+#stepper div:not(:last-child)::before {
+ position: absolute;
+ inset: 13px 0 0 calc(50% + 20px);
+ border-radius: 8px;
+ width: calc(100% - 24px);
+ height: 6px;
+ background-color: var(--control-border-color);
+ content: "";
+}
+
+#stepper span {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ border-radius: 50%;
+ width: 32px;
+ height: 32px;
+ background-color: var(--control-border-color);
+ font-size: 20px;
+}
+
+#stepper div[aria-current="step"] {
+ color: var(--primary-label-color);
+ font-weight: 500;
+}
+
+#stepper div:is(.done, [aria-current="step"]) span {
+ color: var(--inverted-label-color);
+ background-color: var(--selected-tab-border-color);
+}
+
+#stepper div.done::before {
+ background-color: var(--selected-tab-border-color);
+}
+
#steps {
- margin-left: auto;
- margin-right: auto;
+ flex: auto;
+ margin-inline: auto;
max-width: 1200px;
}
-.product-icon {
- float: left;
- margin: 8px 15px 8px 0;
- width: 64px;
- height: 64px;
+#steps h2 {
+ margin-block: 0 16px;
+ font-size: var(--font-size-h3);
+ font-weight: normal;
+ text-align: center;
}
-#product_step {
- margin-left: auto;
- margin-right: auto;
+#page-footer {
+ flex: none;
+ margin-top: 50px;
+ text-align: center;
+}
+
+#advanced-link {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+}
+
+/**
+ * products/webdev step
+ */
+
+:is(#product-step, #webdev-step) {
+ margin-inline: auto;
max-width: 1200px;
}
-ul.product-list {
- display: flex;
- flex-wrap: wrap;
- margin: 0 -10px 20px;
+:is(#product-step, #webdev-step) > section {
+ margin-block: 32px;
+}
+
+.product-list {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
+ gap: 16px;
padding: 0;
list-style: outside none none;
}
-ul.product-list > li {
- display: inline-block;
+.product-list svg,
+.product-icon {
+ width: 64px;
+ height: 64px;
+}
+
+.product-list li {
+ display: flex;
+ align-items: center;
+ gap: 24px;
position: relative;
z-index: 1;
- margin: 4px;
border: 1px solid var(--primary-region-border-color);
border-radius: var(--primary-region-border-radius);
- padding: 1px;
- width: 300px;
+ padding: 24px;
background-color: var(--primary-region-background-color);
background-clip: padding-box;
box-shadow: var(--primary-region-box-shadow);
@@ -65,147 +220,214 @@ ul.product-list > li {
cursor: pointer;
}
-ul.product-list > li > .product-item {
- display: block;
- padding: 10px;
+.product-list li:hover,
+.product-list li:focus {
+ background-color: var(--primary-region-header-background-color);
}
-#steps a img {
- border: none;
+.product-list h3 {
+ margin: 0;
+ line-height: var(--line-height-default);
}
-#advanced {
- margin-top: 50px;
+.product-list p {
+ margin-block: 4px 0;
+}
+
+/**
+ * other-products step
+ */
+
+#other-products {
+ margin-top: 32px;
+}
+
+#other-products .classification :is(th, td) {
+ font-size: var(--font-size-x-large);
+ font-weight: 500;
+}
+
+#other-products :is(th, td) {
+ padding: 4px 8px;
+ line-height: var(--line-height-comfortable);
+ vertical-align: top;
+}
+
+#other-products th {
text-align: right;
- margin-left: auto;
}
-#advanced img {
- vertical-align: middle;
+#prod-comp-search-main {
+ margin: 32px auto;
+ width: 400px;
}
-#advanced a {
- cursor: pointer;
+#prod-comp-search-main .pcs-header {
+ margin-bottom: 4px;
}
/**
- * products and other_products step
+ * duplicates step
*/
-#prod_comp_search_main {
- width: 400px;
+#dupe-header {
+ display: flex;
+ align-items: center;
+ gap: 32px;
+ margin-block: 16px;
}
-#prod_comp_search_label {
- margin-bottom: 1px;
+#dupe-header .product,
+#dupe-header .messages {
+ flex: auto;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 8px;
+ min-width: 200px;
+ text-align: left;
}
-#products {
- width: 600px;
+#dupe-header .messages {
+ list-style: none;
}
-#products td {
- padding: 5px;
- padding-bottom: 10px;
+#dupe-header .messages li:not([hidden]) {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin: 0;
}
-#products h2 {
- margin-bottom: 0;
+#dupe-header .product .name {
+ font-size: var(--font-size-h3);
}
-#products p {
- margin-top: 0;
+#dupe-finder {
+ margin-block: 16px;
}
-.product_img {
- width: 64px;
+#dupe-form {
+ text-align: center;
}
-#other_products .classification {
- font-weight: bold;
+#dupe-form .input {
+ display: flex;
+ justify-content: center;
+ align-items: flex-start;
+ gap: 8px;
+ margin: 0 auto;
+ max-width: 600px;
+ text-align: left;
}
-#other_products .classification th {
- font-size: var(--font-size-x-large);
+#dupe-form .input > :first-child {
+ flex: auto;
+ width: 100%;
}
-/**
- * duplicates step
- */
+#dupe-summary {
+ width: 100%;
+}
-#dupes_summary {
- width: 500px;
+#dupe-search {
+ white-space: nowrap;
}
-#dupes_list {
- margin-top: 1em;
- margin-bottom: 1em;
+#dupe-list {
+ margin-block: 16px;
}
-#product_support {
- border: 1px solid var(--primary-region-border-color);
+#dupe-list .message,
+#dupe-continue {
+ text-align: center;
}
/**
* bug form step
*/
-#bugForm {
- border: 1px solid var(--primary-region-border-color);
- border-radius: var(--primary-region-border-radius);
- width: 600px;
- background-color: var(--primary-region-background-color);
- box-shadow: var(--primary-region-box-shadow);
+#bug-form-inner {
+ margin: 0 auto;
+ display: flex;
+ flex-direction: column;
+ gap: 32px;
}
-#bugForm th,
-#bugForm td {
- padding: 8px 16px;
+#bug-form .messages {
+ margin-block: 16px;
+ padding-left: 32px;
}
-#bugForm .label {
- text-align: left;
- font-weight: bold;
- white-space: nowrap;
+
+#bug-form .row {
+ display: flex;
+ gap: 32px;
}
-#bugzilla-body #bugForm th {
- vertical-align: middle;
+#bug-form .col {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
}
-#bugForm .textInput {
- width: 450px;
+#bug-form .col.main {
+ flex: auto;
}
-#bugForm textarea {
- width: 590px;
+#bug-form .col.side {
+ flex: none;
+ width: 320px;
}
-#bugForm .mandatory_mark {
- color: var(--required-label-color);
- font-size: var(--font-size-small);
+#bug-form .col.side > section:not(:last-child) {
+ border-bottom: 1px solid var(--control-border-color);
+ padding-bottom: 16px;
}
-#versionTD {
- text-align: right;
- white-space: nowrap;
+#bug-form .col > section > header {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 8px;
}
-#component_select {
- width: 450px;
+#bug-form .col > section > header h3 {
+ margin: 0;
+ font-size: var(--font-size-x-large);
+ font-weight: 500;
}
-#component_description {
- padding: 5px;
+#bug-form .col > section > header h3 label span {
+ color: var(--tertiary-label-color);
}
-#bugForm .missing {
- border: 1px solid var(--error-message-foreground-color);
- box-shadow: 0 0 4px var(--error-message-foreground-color);
+#bug-form input[type="text"],
+#bug-form .text-editor,
+#version-select,
+#component-select {
+ width: 100%;
}
-#submitTD {
- text-align: right;
+#bug-form .required-star {
+ color: var(--required-label-color);
+}
+
+#bug-form :is(.checkbox, .radio) {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+#bug-form .checkbox {
+ margin-block: 8px;
+}
+
+#bug-form .radio {
+ margin-block: 4px;
+}
+
+#bug-form :is(.checkbox, .radio) input {
+ flex: none;
}
.help {
@@ -213,22 +435,89 @@ ul.product-list > li > .product-item {
z-index: 9999;
border: 1px solid var(--menu-border-color);
border-radius: var(--menu-border-radius);
- padding: 8px;
+ padding: 12px;
width: 320px;
background: var(--menu-background-color);
box-shadow: var(--menu-box-shadow);
cursor: default;
+ font-weight: normal;
+ white-space: normal;
}
-.help-bad {
- color: var(--error-message-foreground-color);
+.help p {
+ margin-block: 8px 0;
}
-.help-good {
- color: var(--positive-message-foreground-color);
+.help p:first-child {
+ margin-top: 0;
}
-#guided-markdown-help {
- float: right;
- font-size: var(--font-size-small);
+#product-display {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+}
+
+#component-description {
+ padding: 5px;
+}
+
+#submit-row {
+ text-align: right;
+}
+
+@media (width < 1024px) {
+ .product-list {
+ gap: 8px;
+ }
+
+ .product-list svg,
+ .product-icon {
+ width: 48px;
+ height: 48px;
+ }
+
+ .product-list li {
+ gap: 16px;
+ padding: 16px;
+ }
+
+ #bug-form .row {
+ flex-direction: column;
+ }
+
+ #bug-form .col.side {
+ width: 100%;
+ }
+}
+
+@media (width < 768px) {
+ #guided :is([role="region"], [role="search"]) {
+ padding: 16px;
+ }
+
+ #stepper {
+ display: none;
+ }
+
+ #dupe-form .input {
+ flex-direction: column;
+ align-items: center;
+ }
+
+ #dupe-header {
+ flex-direction: column;
+ align-items: normal;
+ gap: 16px;
+ }
+
+ #dupe-header .product {
+ flex-direction: row;
+ align-items: center;
+ }
+
+ #dupe-header .product .name {
+ white-space: normal;
+ }
}
diff --git a/js/attachment.js b/js/attachment.js
index 19935e41a8..bfe2d2f85e 100644
--- a/js/attachment.js
+++ b/js/attachment.js
@@ -263,16 +263,16 @@ function handleWantsAttachment(wants_attachment) {
else {
showElementById('attachment_false');
hideElementById('attachment_true');
- bz_attachment_form.reset_fields();
+ bzAttachmentForm.resetFields();
}
- bz_attachment_form.update_requirements(wants_attachment);
+ bzAttachmentForm.updateRequirements(wants_attachment);
}
/**
* Expose an `AttachmentForm` instance on global.
*/
-var bz_attachment_form;
+var bzAttachmentForm;
/**
* Reference or define the Bugzilla app namespace.
@@ -281,226 +281,334 @@ var bz_attachment_form;
var Bugzilla = Bugzilla || {};
/**
- * Implement the attachment selector functionality that can be used standalone or on the New Bug page. This supports 3
- * input methods: traditional `` field, drag & dropping of a file or text, as well as copy & pasting
- * an image or text.
+ * Implement the attachment selector functionality that can be used standalone or on the New Bug
+ * page. This supports multiple input methods:
+ * - Drag & drop: the user drags a file or text and drops it on the drop target. The drop target is
+ * highlighted when a file is dragged over it.
+ * - Browse: the user clicks the Browse button and selects a file with the file picker dialog.
+ * - Enter text: the user clicks the Enter Text button and enters text in the textarea.
+ * - Paste: the user clicks the Paste button and pastes an image or text from the clipboard. This is
+ * supported only in browsers with the Async Clipboard API. In other browsers, the Paste button is
+ * hidden.
+ * - Capture: the user clicks the Take a Screenshot button and captures a screen, window or browser
+ * tab. The capture is attached as a PNG image.
*/
-Bugzilla.AttachmentForm = class AttachmentForm {
+Bugzilla.AttachmentSelector = class AttachmentSelector {
/**
- * Initialize a new `AttachmentForm` instance.
+ * Initialize a new `AttachmentSelector` instance.
+ * @param {object} params An object of parameters.
+ * @param {HTMLElement} params.$placeholder An element to be enhanced with the attachment selector
+ * functionality.
+ * @param {Record} [params.eventHandlers] An object of event handlers to be
+ * called when the user performs certain actions.
*/
- constructor() {
- this.$file = document.querySelector('#att-file');
- this.$data = document.querySelector('#att-data');
- this.$filename = document.querySelector('#att-filename');
- this.$dropbox = document.querySelector('#att-dropbox');
- this.$browse_label = document.querySelector('#att-browse-label');
- this.$capture_label = document.querySelector('#att-capture-label');
- this.$textarea = document.querySelector('#att-textarea');
- this.$preview = document.querySelector('#att-preview');
- this.$preview_name = this.$preview.querySelector('[itemprop="name"]');
- this.$preview_type = this.$preview.querySelector('[itemprop="encodingFormat"]');
- this.$preview_text = this.$preview.querySelector('[itemprop="text"]');
- this.$preview_image = this.$preview.querySelector('[itemprop="image"]');
- this.$remove_button = document.querySelector('#att-remove-button');
- this.$description = document.querySelector('#att-description');
- this.$error_message = document.querySelector('#att-error-message');
- this.$ispatch = document.querySelector('#att-ispatch');
- this.$hide_preview = document.querySelector('#att-hide-preview');
- this.$type_outer = document.querySelector('#att-type-outer');
- this.$type_list = document.querySelector('#att-type-list');
- this.$type_manual = document.querySelector('#att-type-manual');
- this.$type_select = document.querySelector('#att-type-select');
- this.$type_input = document.querySelector('#att-type-input');
- this.$isprivate = document.querySelector('#isprivate');
- this.$takebug = document.querySelector('#takebug');
-
- // Add event listeners
- this.$file.addEventListener('change', () => this.file_onchange());
- this.$dropbox.addEventListener('dragover', event => this.dropbox_ondragover(event));
- this.$dropbox.addEventListener('dragleave', () => this.dropbox_ondragleave());
- this.$dropbox.addEventListener('dragend', () => this.dropbox_ondragend());
- this.$dropbox.addEventListener('drop', event => this.dropbox_ondrop(event));
- this.$browse_label.addEventListener('click', () => this.$file.click());
- this.$capture_label.addEventListener('click', () => this.capture_onclick());
- this.$textarea.addEventListener('input', () => this.textarea_oninput());
- this.$textarea.addEventListener('paste', event => this.textarea_onpaste(event));
- this.$remove_button.addEventListener('click', () => this.remove_button_onclick());
- this.$description.addEventListener('input', () => this.description_oninput());
- this.$description.addEventListener('change', () => this.description_onchange());
- this.$ispatch.addEventListener('change', () => this.ispatch_onchange());
- this.$hide_preview.addEventListener('change', () => this.hide_preview_onchange());
- this.$type_select.addEventListener('change', () => this.type_select_onchange());
- this.$type_input.addEventListener('change', () => this.type_input_onchange());
-
- // Prepare the file reader
- this.data_reader = new FileReader();
- this.text_reader = new FileReader();
- this.data_reader.addEventListener('load', () => this.data_reader_onload());
- this.text_reader.addEventListener('load', () => this.text_reader_onload());
-
- // Initialize the view
- this.enable_keyboard_access();
- this.reset_fields();
+ constructor({ $placeholder, eventHandlers = {} }) {
+ this.$placeholder = $placeholder;
+ this.eventHandlers = eventHandlers;
+
+ this.#renderUI();
+ this.#cacheElements();
+ this.#setupEventListeners();
+ this.#setupFileReaders();
+ this.#initializeView();
+ this.#checkBrowserSupport();
}
/**
- * Enable keyboard access on the buttons. Treat the Enter keypress as a click.
+ * Render the attachment selector UI template, detecting device type to conditionally show
+ * drag-and-drop hint.
*/
- enable_keyboard_access() {
- document.querySelectorAll('#att-selector [role="button"]').forEach($button => {
- $button.addEventListener('keypress', event => {
- if (!event.isComposing && event.key === 'Enter') {
- event.target.click();
- }
- });
- });
+ #renderUI() {
+ // Assume a fine pointer (e.g., a mouse) means a desktop device, and a coarse pointer means a
+ // mobile device. Don’t show the “Drag & drop” hint on mobile because it’s not a common input
+ // method on that platform.
+ const isDesktop = window.matchMedia('(pointer: fine)').matches;
+
+ this.$placeholder.innerHTML = `
+
+
+
+
+ ${isDesktop ? `Drag & drop a file here, or` : ''}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
}
/**
- * Reset all the input fields to the initial state, and remove the preview and message.
+ * Cache references to DOM elements for quick access.
*/
- reset_fields() {
- this.description_override = false;
- this.$file.value = this.$data.value = this.$filename.value = this.$type_input.value = this.$description.value = '';
- this.$type_list.checked = this.$type_select.options[0].selected = true;
+ #cacheElements() {
+ this.$file = this.$placeholder.querySelector('#att-file');
+ this.$data = this.$placeholder.querySelector('#att-data');
+ this.$filename = this.$placeholder.querySelector('#att-filename');
+ this.$dropbox = this.$placeholder.querySelector('#att-dropbox');
+ this.$selectorActions = this.$placeholder.querySelector('#att-selector .actions');
+ this.$browseButton = this.$placeholder.querySelector('#att-browse-button');
+ this.$enterButton = this.$placeholder.querySelector('#att-enter-button');
+ this.$pasteButton = this.$placeholder.querySelector('#att-paste-button');
+ this.$captureButton = this.$placeholder.querySelector('#att-capture-button');
+ this.$editor = this.$placeholder.querySelector('#att-editor');
+ this.$textarea = this.$editor.querySelector('#att-textarea');
+ this.$preview = this.$placeholder.querySelector('#att-preview');
+ this.$previewName = this.$preview.querySelector('[itemprop="name"]');
+ this.$previewType = this.$preview.querySelector('[itemprop="encodingFormat"]');
+ this.$previewText = this.$preview.querySelector('[itemprop="text"]');
+ this.$previewImage = this.$preview.querySelector('[itemprop="image"]');
+ this.$textRemoveButton = this.$placeholder.querySelector('#att-text-remove-button');
+ this.$fileRemoveButton = this.$placeholder.querySelector('#att-file-remove-button');
+ this.$errorMessage = this.$placeholder.querySelector('#att-error-message');
+ }
- if (this.$isprivate) {
- this.$isprivate.checked = this.$isprivate.disabled = false;
- }
+ /**
+ * Register all event listeners for form submission, file input, drag-and-drop, buttons, and text
+ * input.
+ */
+ #setupEventListeners() {
+ this.$placeholder.closest('form').addEventListener('submit', (event) => this.validate(event));
+ this.$placeholder.closest('form').querySelector('[type="submit"]')
+ ?.addEventListener('click', (event) => this.validate(event));
+ this.$file.addEventListener('change', () => this.fileOnChange());
+ this.$dropbox.addEventListener('dragover', (event) => this.dropboxOnDragOver(event));
+ this.$dropbox.addEventListener('dragleave', () => this.dropboxOnDragLeave());
+ this.$dropbox.addEventListener('dragend', () => this.dropboxOnDragEnd());
+ this.$dropbox.addEventListener('drop', (event) => this.dropboxOnDrop(event));
+ this.$browseButton.addEventListener('click', () => this.$file.click());
+ this.$enterButton.addEventListener('click', () => this.enterButtonOnClick());
+ this.$pasteButton.addEventListener('click', () => this.pasteButtonOnClick());
+ this.$captureButton.addEventListener('click', () => this.captureButtonOnClick());
+ this.$textarea.addEventListener('input', () => this.textareaOnInput());
+ this.$textRemoveButton.addEventListener('click', () => this.removeButtonOnClick());
+ this.$fileRemoveButton.addEventListener('click', () => this.removeButtonOnClick());
+ }
- if (this.$takebug) {
- this.$takebug.checked = this.$takebug.disabled = false;
- }
+ /**
+ * Initialize FileReader instances for reading file data and text content.
+ */
+ #setupFileReaders() {
+ this.dataReader = new FileReader();
+ this.textReader = new FileReader();
+ this.dataReader.addEventListener('load', () => this.dataReaderOnLoad());
+ this.textReader.addEventListener('load', () => this.textReaderOnLoad());
+ }
- this.clear_preview();
- this.clear_error();
- this.update_requirements();
- this.update_text();
- this.update_ispatch();
+ /**
+ * Initialize the UI state and prepare the form for use.
+ */
+ #initializeView() {
+ this.enableKeyboardAccess();
+ this.resetFields();
}
/**
- * Update the `required` property on the Base64 data and Description fields.
- * @param {Boolean} [required=true] `true` if these fields are required, `false` otherwise.
+ * Hide action buttons if the required browser APIs are not supported.
*/
- update_requirements(required = true) {
- this.$data.required = this.$description.required = required;
- this.update_validation();
+ #checkBrowserSupport() {
+ // Hide the Paste button if the Clipboard API is not available
+ this.$pasteButton.hidden = typeof navigator.clipboard?.read !== 'function';
+ // Hide the Capture button if the Screen Capture API is not available
+ this.$captureButton.hidden = typeof navigator.mediaDevices?.getDisplayMedia !== 'function';
}
/**
- * Update the custom validation message on the Base64 data field depending on the requirement and value.
+ * Whether the attachment is required.
+ * @type {boolean}
*/
- update_validation() {
- this.$data.setCustomValidity(this.$data.required && !this.$data.value ? 'Please select a file or enter text.' : '');
+ get required() {
+ return this.$placeholder.dataset.required === 'true';
+ }
- // In Firefox, the message won't be displayed once the field becomes valid then becomes invalid again. This is a
- // workaround for the issue.
- this.$data.hidden = false;
- this.$data.hidden = true;
+ /**
+ * Show or hide the header with the action buttons.
+ * @param {boolean} value `true` to show the header, `false` to hide it.
+ */
+ set actionsDisplayed(value) {
+ this.$selectorActions.hidden = !value;
}
/**
- * Process a user-selected file for upload. Read the content if it's been transferred with a paste or drag operation.
- * Update the Description, Content Type, etc. and show the preview.
+ * Show or hide the editor.
+ * @param {boolean} value `true` to show the editor, `false` to hide it.
+ */
+ set editorDisplayed(value) {
+ this.$editor.hidden = !value;
+ }
+
+ /**
+ * Show or hide the preview.
+ * @param {boolean} value `true` to show the preview, `false` to hide it.
+ */
+ set previewDisplayed(value) {
+ this.$preview.hidden = !value;
+ }
+
+ /**
+ * Dispatch a custom event to the registered event handlers.
+ * @param {string} name The name of the event.
+ * @param {Record} [detail] Additional data to pass to the event handler.
+ */
+ dispatchEvent(name, detail = {}) {
+ this.eventHandlers[name]?.(detail);
+ }
+
+ /**
+ * Enable keyboard access on the buttons. Treat the Enter keypress as a click.
+ */
+ enableKeyboardAccess() {
+ document.querySelectorAll('#att-selector [role="button"]').forEach(($button) => {
+ $button.addEventListener('keypress', (event) => {
+ if (!event.isComposing && event.key === 'Enter') {
+ event.target.click();
+ }
+ });
+ });
+ }
+
+ /**
+ * Reset all the input fields to the initial state, and remove the preview and message.
+ */
+ resetFields() {
+ this.$file.value = '';
+ this.$data.value = '';
+ this.$filename.value = '';
+
+ this.clearPreview();
+ this.clearError();
+ this.updateText();
+ }
+
+ /**
+ * Process a file for upload regardless of how it was provided (file picker, drag-and-drop, paste
+ * or screen capture). Read the file content, update the filename field, and show the preview.
* @param {File} file A file to be read.
- * @param {Boolean} [transferred=true] `true` if the source is `DataTransfer`, `false` if it's been selected via
- * ``.
*/
- process_file(file, transferred = true) {
+ processFile(file) {
// Detect patches that should have the `text/plain` MIME type
- const is_patch = !!file.name.match(/\.(?:diff|patch)$/) || !!file.type.match(/^text\/x-(?:diff|patch)$/);
- // Detect Markdown files that should have `text/plain` instead of `text/markdown` due to Firefox Bug 1421032
- const is_markdown = !!file.name.match(/\.(?:md|mkdn?|mdown|markdown)$/);
+ const isPatch =
+ !!file.name.match(/\.(?:diff|patch)$/) || !!file.type.match(/^text\/x-(?:diff|patch)$/);
+ // Detect Markdown files that should have `text/plain` instead of `text/markdown` due to Firefox
+ // Bug 1421032
+ const isMarkdown = !!file.name.match(/\.(?:md|mkdn?|mdown|markdown)$/);
// Detect common source files that may have no MIME type or `application/*` MIME type
- const is_source = !!file.name.match(/\.(?:cpp|es|h|js|json|rs|rst|sh|toml|ts|tsx|xml|yaml|yml)$/);
+ const isSource = !!file.name.match(
+ /\.(?:cpp|es|h|js|json|rs|rst|sh|toml|ts|tsx|xml|yaml|yml)$/,
+ );
// Detect any plaintext file
- const is_text = file.type.startsWith('text/') || is_patch || is_markdown || is_source;
- // Reassign the MIME type: use `text/plain` for most text files and `application/octet-stream` as a fallback
- const type = (is_patch || is_markdown || (is_source && !file.type)) ?
- 'text/plain' : (file.type || 'application/octet-stream');
-
- if (this.check_file_size(file.size)) {
- this.$data.required = transferred;
-
- if (transferred) {
- this.data_reader.readAsDataURL(file);
- this.$file.value = '';
- this.$filename.value = file.name.replace(/\s/g, '-');
- } else {
- this.$data.value = this.$filename.value = '';
- }
+ const isText = file.type.startsWith('text/') || isPatch || isMarkdown || isSource;
+ // Reassign the MIME type: use `text/plain` for most text files and `application/octet-stream`
+ // as a fallback
+ const type =
+ isPatch || isMarkdown || (isSource && !file.type)
+ ? 'text/plain'
+ : file.type || 'application/octet-stream';
+
+ if (this.checkFileSize(file.size)) {
+ this.dataReader.readAsDataURL(file);
+ this.$file.value = '';
+ this.$filename.value = file.name.replace(/\s/g, '-');
} else {
- this.$data.required = true;
- this.$file.value = this.$data.value = this.$filename.value = '';
- }
-
- this.update_validation();
- this.show_preview(file, is_text);
- this.update_text();
- this.update_content_type(type);
- this.update_ispatch(is_patch);
-
- if (!this.description_override) {
- this.$description.value = file.name;
+ this.$file.value = '';
+ this.$data.value = '';
+ this.$filename.value = '';
}
- this.$description.select();
- this.$description.focus();
+ this.showPreview(file, isText);
+ this.updateText();
+ this.dispatchEvent('AttachmentProcessed', { file, type, isPatch });
}
/**
- * Check the current file size and show an error message if it exceeds the application-defined limit.
- * @param {Number} size A file size in bytes.
- * @returns {Boolean} `true` if the file is less than the maximum allowed size, `false` otherwise.
+ * Check the current file size and show an error message if it exceeds the application-defined
+ * limit.
+ * @param {number} size A file size in bytes.
+ * @returns {boolean} `true` if the file is less than the maximum allowed size, `false` otherwise.
*/
- check_file_size(size) {
- const file_size = size / 1024; // Convert to KB
- const max_size = BUGZILLA.param.maxattachmentsize; // Defined in KB
- const invalid = file_size > max_size;
- const message = invalid ?
- `This file (${(file_size / 1024).toFixed(1)} MB) is larger than the maximum allowed size ` +
- `(${(max_size / 1024).toFixed(1)} MB). Please consider uploading it to an online file storage ` +
- 'and sharing the link in a bug comment instead.' : '';
- const message_short = invalid ? 'File too large' : '';
-
- this.$error_message.innerHTML = message;
- this.$textarea.setCustomValidity(message_short);
- this.$textarea.setAttribute('aria-invalid', invalid);
+ checkFileSize(size) {
+ const fileSize = size / 1024; // Convert to KB
+ const maxSize = BUGZILLA.param.maxattachmentsize; // Defined in KB
+ const invalid = fileSize > maxSize;
+ const message = invalid
+ ? `This file (${(fileSize / 1024).toFixed(1)} MB) is larger than the ` +
+ `maximum allowed size (${(maxSize / 1024).toFixed(1)} MB). Please ` +
+ `consider uploading it to an online file storage and sharing the link in a ` +
+ `${BUGZILLA.string.bug} comment instead.`
+ : '';
+ const messageShort = invalid ? 'File too large' : '';
+
+ this.$errorMessage.hidden = !invalid;
+ this.$errorMessage.innerHTML = message;
this.$dropbox.classList.toggle('invalid', invalid);
return !invalid;
}
/**
- * Called whenever a file's data URL is read by `FileReader`. Embed the Base64-encoded content for upload.
+ * Called whenever a file’s data URL is read by `FileReader`. Embed the Base64-encoded content for
+ * upload.
*/
- data_reader_onload() {
- this.$data.value = this.data_reader.result.split(',')[1];
- this.update_validation();
+ dataReaderOnLoad() {
+ this.$data.value = this.dataReader.result.split(',')[1];
}
/**
- * Called whenever a file's text content is read by `FileReader`. Show the preview of the first 10 lines.
+ * Called whenever a file’s text content is read by `FileReader`. Show the preview of the first 10
+ * lines.
*/
- text_reader_onload() {
- this.$preview_text.textContent = this.text_reader.result.split(/\r\n|\r|\n/, 10).join('\n');
+ textReaderOnLoad() {
+ this.$previewText.textContent = this.textReader.result.split(/\r\n|\r|\n/, 10).join('\n');
}
/**
* Called whenever a file is selected by the user by using the file picker. Prepare for upload.
*/
- file_onchange() {
- this.process_file(this.$file.files[0], false);
+ fileOnChange() {
+ this.processFile(this.$file.files[0]);
}
/**
- * Called whenever a file is being dragged on the drop target. Allow the `copy` drop effect, and set a class name on
- * the drop target for styling.
+ * Called whenever a file is being dragged on the drop target. Allow the `copy` drop effect, and
+ * set a class name on the drop target for styling.
* @param {DragEvent} event A `dragover` event.
*/
- dropbox_ondragover(event) {
+ dropboxOnDragOver(event) {
event.preventDefault();
event.dataTransfer.dropEffect = event.dataTransfer.effectAllowed = 'copy';
@@ -512,280 +620,627 @@ Bugzilla.AttachmentForm = class AttachmentForm {
/**
* Called whenever a dragged file leaves the drop target. Reset the styling.
*/
- dropbox_ondragleave() {
+ dropboxOnDragLeave() {
this.$dropbox.classList.remove('dragover');
}
/**
* Called whenever a drag operation is being ended. Reset the styling.
*/
- dropbox_ondragend() {
+ dropboxOnDragEnd() {
this.$dropbox.classList.remove('dragover');
}
/**
- * Called whenever a file or text is dropped on the drop target. If it's a file, read the content. If it's plaintext,
- * fill in the textarea.
+ * Called whenever a file or text is dropped on the drop target. If it’s a file, read the content.
+ * If it’s plaintext, fill in the textarea.
* @param {DragEvent} event A `drop` event.
*/
- dropbox_ondrop(event) {
+ dropboxOnDrop(event) {
event.preventDefault();
const files = event.dataTransfer.files;
const text = event.dataTransfer.getData('text');
if (files.length > 0) {
- this.process_file(files[0]);
+ this.processFile(files[0]);
+ this.editorDisplayed = false;
+ this.previewDisplayed = true;
} else if (text) {
- this.clear_preview();
- this.clear_error();
- this.update_text(text);
+ this.clearPreview();
+ this.clearError();
+ this.updateText(text);
+ this.actionsDisplayed = false;
+ this.editorDisplayed = true;
+ this.previewDisplayed = false;
}
this.$dropbox.classList.remove('dragover');
}
/**
- * Insert text to the textarea, and show it if it's not empty.
- * @param {String} [text=''] Text to be inserted.
+ * Insert text to the textarea, and show it if it’s not empty.
+ * @param {string} [text] Text to be inserted.
*/
- update_text(text = '') {
+ updateText(text = '') {
this.$textarea.value = text;
- this.textarea_oninput();
+ this.textareaOnInput();
- if (text) {
+ if (text.trim()) {
this.$textarea.hidden = false;
+ this.$dropbox.classList.remove('invalid');
+ this.$errorMessage.hidden = true;
}
}
/**
- * Called whenever the Take a Screenshot button is clicked. Capture a screen, window or browser tab if the Screen
- * Capture API is supported, then attach it as a PNG image.
- * @see https://developer.mozilla.org/en-US/docs/Web/API/Screen_Capture_API
+ * Called whenever the Enter Text button is clicked. Show the textarea for text input.
*/
- capture_onclick() {
- if (typeof navigator.mediaDevices.getDisplayMedia !== 'function') {
- alert('This function requires the most recent browser version such as Firefox 66 or Chrome 72.');
- return;
+ enterButtonOnClick() {
+ this.actionsDisplayed = false;
+ this.editorDisplayed = true;
+ this.$textarea.focus();
+ }
+
+ /**
+ * Called whenever the Paste button is clicked. Read the clipboard content and process it. This
+ * supports pasting of regular images, links and text.
+ */
+ async pasteButtonOnClick() {
+ try {
+ const items = [...(await navigator.clipboard.read())];
+ let pasted = false;
+
+ // Process only the first item until multiple items are supported
+ items.length = 1;
+
+ for (const item of items) {
+ if (item.types.includes('image/png')) {
+ const blob = await item.getType('image/png');
+ const file = new File([blob], 'pasted-image.png', { type: 'image/png' });
+
+ this.processFile(file);
+ this.editorDisplayed = false;
+ this.previewDisplayed = true;
+ pasted = true;
+ } else if (item.types.includes('text/plain')) {
+ const blob = await item.getType('text/plain');
+ const text = await blob.text();
+
+ this.updateText(text);
+ this.editorDisplayed = true;
+ this.previewDisplayed = false;
+ pasted = true;
+ }
+ }
+
+ if (pasted) {
+ this.actionsDisplayed = false;
+ this.dispatchEvent('AttachmentPasted', { items });
+ } else {
+ alert('No image or text data found in the clipboard.');
+ }
+ } catch (error) {
+ alert(error.message);
}
+ }
+ /**
+ * Called whenever the Take a Screenshot button is clicked. Capture a screen, window or browser
+ * tab if the Screen Capture API is supported, then attach it as a PNG image.
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/Screen_Capture_API
+ */
+ async captureButtonOnClick() {
const $video = document.createElement('video');
const $canvas = document.createElement('canvas');
- navigator.mediaDevices.getDisplayMedia({ video: { displaySurface: 'window' } }).then(stream => {
+ try {
+ const stream = await navigator.mediaDevices.getDisplayMedia({
+ video: { displaySurface: 'window' },
+ });
+
// Render a captured screenshot on `