mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-03-19 23:28:23 -04:00
Update form detection logic, update tests (#541)
This commit is contained in:
@@ -18,54 +18,18 @@ export class FormDetector {
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect login forms on the page, prioritizing the form containing the clicked element.
|
||||
* Detect login forms on the page based on the clicked element.
|
||||
*/
|
||||
public detectForms(): LoginForm[] {
|
||||
// If we have a clicked element, try to find its form first
|
||||
if (this.clickedElement) {
|
||||
const formWrapper = this.clickedElement.closest('form');
|
||||
const formWrapper = this.clickedElement.closest('form') ?? this.document.body;
|
||||
|
||||
if (formWrapper) {
|
||||
// Check if the wrapper contains a password or likely username field before processing.
|
||||
if (this.containsPasswordField(formWrapper) || this.containsLikelyUsernameOrEmailField(formWrapper)) {
|
||||
this.createFormEntry(formWrapper);
|
||||
|
||||
// If we found a valid form, return early
|
||||
if (this.forms.length > 0) {
|
||||
return this.forms;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find all input fields that could be part of a login form
|
||||
const passwordFields = this.document.querySelectorAll<HTMLInputElement>('input[type="password"]');
|
||||
const emailFields = this.document.querySelectorAll<HTMLInputElement>('input[type="email"], input[type="text"]');
|
||||
const textFields = this.document.querySelectorAll<HTMLInputElement>('input[type="text"]');
|
||||
|
||||
// Process password fields first
|
||||
passwordFields.forEach(passwordField => {
|
||||
const form = passwordField.closest('form');
|
||||
this.createFormEntry(form);
|
||||
});
|
||||
|
||||
// Process email fields that aren't already part of a processed form
|
||||
emailFields.forEach(field => {
|
||||
const form = field.closest('form');
|
||||
if (form && this.processedForms.has(form)) return;
|
||||
|
||||
if (this.isLikelyEmailField(field)) {
|
||||
this.createFormEntry(form);
|
||||
}
|
||||
});
|
||||
|
||||
// Process potential username fields that aren't already part of a processed form
|
||||
textFields.forEach(field => {
|
||||
const form = field.closest('form');
|
||||
if (form && this.processedForms.has(form)) return;
|
||||
|
||||
if (this.isLikelyUsernameField(field)) {
|
||||
this.createFormEntry(form);
|
||||
}
|
||||
});
|
||||
|
||||
return this.forms;
|
||||
}
|
||||
|
||||
@@ -318,38 +282,6 @@ export class FormDetector {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a field is likely an email field based on its attributes
|
||||
*/
|
||||
private isLikelyEmailField(input: HTMLInputElement): boolean {
|
||||
const attributes = [
|
||||
input.type,
|
||||
input.id,
|
||||
input.name,
|
||||
input.className,
|
||||
input.placeholder
|
||||
].map(attr => attr?.toLowerCase() ?? '');
|
||||
|
||||
const patterns = ['email', 'e-mail', 'mail', 'address', '@'];
|
||||
return patterns.some(pattern => attributes.some(attr => attr.includes(pattern)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a field is likely a username field based on its attributes
|
||||
*/
|
||||
private isLikelyUsernameField(input: HTMLInputElement): boolean {
|
||||
const attributes = [
|
||||
input.type,
|
||||
input.id,
|
||||
input.name,
|
||||
input.className,
|
||||
input.placeholder
|
||||
].map(attr => attr?.toLowerCase() ?? '');
|
||||
|
||||
const patterns = ['user', 'username', 'login', 'identifier'];
|
||||
return patterns.some(pattern => attributes.some(attr => attr.includes(pattern)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the password field in a form.
|
||||
*/
|
||||
@@ -410,46 +342,77 @@ export class FormDetector {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a form contains a password field.
|
||||
*/
|
||||
private containsPasswordField(wrapper: HTMLElement): boolean {
|
||||
const passwordFields = this.findPasswordField(wrapper as HTMLFormElement | null);
|
||||
if (passwordFields.primary) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a form contains a likely username or email field.
|
||||
*/
|
||||
private containsLikelyUsernameOrEmailField(wrapper: HTMLElement): boolean {
|
||||
// Check if the form contains an email field.
|
||||
const emailFields = this.findEmailField(wrapper as HTMLFormElement | null);
|
||||
if (emailFields.primary && emailFields.primary.getAttribute('autocomplete') !== 'off') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if the form contains a username field.
|
||||
const usernameField = this.findInputField(wrapper as HTMLFormElement | null, ['username', 'gebruikersnaam', 'gebruiker', 'login', 'identifier', 'user'], ['text'], []);
|
||||
if (usernameField && usernameField.getAttribute('autocomplete') !== 'off') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a form entry.
|
||||
*/
|
||||
private createFormEntry(form: HTMLFormElement | null): void {
|
||||
private createFormEntry(wrapper: HTMLElement | null): void {
|
||||
// Skip if we've already processed this form
|
||||
if (form && this.processedForms.has(form)) return;
|
||||
this.processedForms.add(form);
|
||||
if (wrapper && this.processedForms.has(wrapper as HTMLFormElement)) return;
|
||||
this.processedForms.add(wrapper as HTMLFormElement);
|
||||
|
||||
// Keep track of detected fields to prevent overlap
|
||||
const detectedFields: HTMLInputElement[] = [];
|
||||
|
||||
// Find fields in priority order (most specific to least specific).
|
||||
const emailFields = this.findEmailField(form);
|
||||
const emailFields = this.findEmailField(wrapper as HTMLFormElement | null);
|
||||
if (emailFields.primary) detectedFields.push(emailFields.primary);
|
||||
if (emailFields.confirm) detectedFields.push(emailFields.confirm);
|
||||
|
||||
const passwordFields = this.findPasswordField(form);
|
||||
const passwordFields = this.findPasswordField(wrapper as HTMLFormElement | null);
|
||||
if (passwordFields.primary) detectedFields.push(passwordFields.primary);
|
||||
if (passwordFields.confirm) detectedFields.push(passwordFields.confirm);
|
||||
|
||||
const usernameField = this.findInputField(form, ['username', 'gebruikersnaam', 'gebruiker', 'login', 'identifier', 'user'],['text'], detectedFields);
|
||||
const usernameField = this.findInputField(wrapper as HTMLFormElement | null, ['username', 'gebruikersnaam', 'gebruiker', 'login', 'identifier', 'user'],['text'], detectedFields);
|
||||
if (usernameField) detectedFields.push(usernameField);
|
||||
|
||||
const firstNameField = this.findInputField(form, ['firstname', 'first-name', 'fname', 'voornaam', 'name'], ['text'], detectedFields);
|
||||
const firstNameField = this.findInputField(wrapper as HTMLFormElement | null, ['firstname', 'first-name', 'fname', 'voornaam', 'name'], ['text'], detectedFields);
|
||||
if (firstNameField) detectedFields.push(firstNameField);
|
||||
|
||||
const lastNameField = this.findInputField(form, ['lastname', 'last-name', 'lname', 'achternaam'], ['text'], detectedFields);
|
||||
const lastNameField = this.findInputField(wrapper as HTMLFormElement | null, ['lastname', 'last-name', 'lname', 'achternaam'], ['text'], detectedFields);
|
||||
if (lastNameField) detectedFields.push(lastNameField);
|
||||
|
||||
const birthdateField = this.findBirthdateFields(form, detectedFields);
|
||||
const birthdateField = this.findBirthdateFields(wrapper as HTMLFormElement | null, detectedFields);
|
||||
if (birthdateField.single) detectedFields.push(birthdateField.single);
|
||||
if (birthdateField.day) detectedFields.push(birthdateField.day);
|
||||
if (birthdateField.month) detectedFields.push(birthdateField.month);
|
||||
if (birthdateField.year) detectedFields.push(birthdateField.year);
|
||||
|
||||
const genderField = this.findGenderField(form, detectedFields);
|
||||
const genderField = this.findGenderField(wrapper as HTMLFormElement | null, detectedFields);
|
||||
if (genderField.field) detectedFields.push(genderField.field as HTMLInputElement);
|
||||
|
||||
this.forms.push({
|
||||
form,
|
||||
form: wrapper as HTMLFormElement,
|
||||
emailField: emailFields.primary,
|
||||
emailConfirmField: emailFields.confirm,
|
||||
usernameField,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe } from 'vitest';
|
||||
import { FormField, testField } from './TestUtils';
|
||||
|
||||
describe('FormDetector', () => {
|
||||
describe('FormDetector English tests', () => {
|
||||
describe('English registration form 1 detection', () => {
|
||||
const htmlFile = 'en-registration-form1.html';
|
||||
|
||||
@@ -26,6 +26,7 @@ describe('FormDetector', () => {
|
||||
describe('English email form 1 detection', () => {
|
||||
const htmlFile = 'en-email-form1.html';
|
||||
|
||||
// Assert that this test fails, because the autocomplete=off for the specified element.
|
||||
testField(FormField.Email, 'P0-0', htmlFile);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { createTestDom } from './TestUtils';
|
||||
import { FormDetector } from '../FormDetector';
|
||||
|
||||
describe('FormDetector generic tests', () => {
|
||||
describe('Invalid form not detected as login form 1', () => {
|
||||
const htmlFile = 'invalid-form1.html';
|
||||
|
||||
it('should not detect any forms', () => {
|
||||
const dom = createTestDom(htmlFile);
|
||||
const document = dom.window.document;
|
||||
const formDetector = new FormDetector(document);
|
||||
const forms = formDetector.detectForms();
|
||||
expect(forms).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Invalid form not detected as login form 2', () => {
|
||||
const htmlFile = 'invalid-form2.html';
|
||||
|
||||
it('should not detect any forms even when clicking search input', () => {
|
||||
const dom = createTestDom(htmlFile);
|
||||
const document = dom.window.document;
|
||||
|
||||
// Pass the search input as the clicked element to test if it's still not detected as a login form.
|
||||
const searchInput = document.getElementById('js-issues-search');
|
||||
const formDetector = new FormDetector(document, searchInput as HTMLElement);
|
||||
const forms = formDetector.detectForms();
|
||||
expect(forms).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Form with autocomplete="off" not detected', () => {
|
||||
const htmlFile = 'autocomplete-off.html';
|
||||
|
||||
it('should not detect form with autocomplete="off" on email field', () => {
|
||||
const dom = createTestDom(htmlFile);
|
||||
const document = dom.window.document;
|
||||
const formDetector = new FormDetector(document);
|
||||
const forms = formDetector.detectForms();
|
||||
expect(forms).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe } from 'vitest';
|
||||
import { FormField, testField, testBirthdateFormat } from './TestUtils';
|
||||
|
||||
describe('FormDetector', () => {
|
||||
describe('FormDetector Dutch tests', () => {
|
||||
describe('Dutch registration form detection', () => {
|
||||
const htmlFile = 'nl-registration-form1.html';
|
||||
|
||||
@@ -35,7 +35,7 @@ describe('FormDetector', () => {
|
||||
testField(FormField.Password, 'password', htmlFile);
|
||||
|
||||
testField(FormField.BirthDate, 'date', htmlFile);
|
||||
testBirthdateFormat('dd-mm-yyyy', htmlFile);
|
||||
testBirthdateFormat('dd-mm-yyyy', htmlFile, 'date');
|
||||
testField(FormField.GenderMale, 'gender1', htmlFile);
|
||||
testField(FormField.GenderFemale, 'gender2', htmlFile);
|
||||
testField(FormField.GenderOther, 'gender3', htmlFile);
|
||||
@@ -55,7 +55,7 @@ describe('FormDetector', () => {
|
||||
testField(FormField.FirstName, 'input_25_14', htmlFile);
|
||||
testField(FormField.LastName, 'input_25_15', htmlFile);
|
||||
testField(FormField.BirthDate, 'input_25_10', htmlFile);
|
||||
testBirthdateFormat('dd/mm/yyyy', htmlFile);
|
||||
testBirthdateFormat('dd/mm/yyyy', htmlFile, 'input_25_10');
|
||||
});
|
||||
|
||||
describe('Dutch registration form 6 detection', () => {
|
||||
|
||||
@@ -5,46 +5,6 @@ import { it, expect } from 'vitest';
|
||||
import { JSDOM } from 'jsdom';
|
||||
import { LoginForm } from '../types/LoginForm';
|
||||
|
||||
/**
|
||||
* Load a test HTML file.
|
||||
*/
|
||||
const loadTestHtml = (filename: string): string => {
|
||||
return readFileSync(join(__dirname, 'test-forms', filename), 'utf-8');
|
||||
};
|
||||
|
||||
/**
|
||||
* Setup a form detection test.
|
||||
*/
|
||||
const setupFormTest = (htmlFile: string, focusedElementId?: string) : { document: Document, result: LoginForm } => {
|
||||
const html = loadTestHtml(htmlFile);
|
||||
const dom = new JSDOM(html, {
|
||||
url: 'http://localhost',
|
||||
runScripts: 'dangerously',
|
||||
resources: 'usable'
|
||||
});
|
||||
const document = dom.window.document;
|
||||
|
||||
// Set focus on specified element if provided
|
||||
let focusedElement: HTMLElement | null = null;
|
||||
if (focusedElementId) {
|
||||
focusedElement = document.getElementById(focusedElementId);
|
||||
if (!focusedElement) {
|
||||
throw new Error(`Focus element with id "${focusedElementId}" not found in test HTML`);
|
||||
}
|
||||
focusedElement.focus();
|
||||
|
||||
// Create a new form detector with the focused element.
|
||||
const formDetector = new FormDetector(document, focusedElement);
|
||||
const result = formDetector.detectForms()[0];
|
||||
return { document, result };
|
||||
}
|
||||
|
||||
// No focused element, so just detect the first form.
|
||||
const formDetector = new FormDetector(document);
|
||||
const result = formDetector.detectForms()[0];
|
||||
return { document, result };
|
||||
};
|
||||
|
||||
export enum FormField {
|
||||
Username = 'username',
|
||||
FirstName = 'firstName',
|
||||
@@ -64,6 +24,19 @@ export enum FormField {
|
||||
GenderOther = 'genderOther'
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a JSDOM instance for a test HTML file that can be used to provide as
|
||||
* input to the form detector logic.
|
||||
*/
|
||||
export const createTestDom = (htmlFile: string) : JSDOM => {
|
||||
const html = loadTestHtml(htmlFile);
|
||||
return new JSDOM(html, {
|
||||
url: 'http://localhost',
|
||||
runScripts: 'dangerously',
|
||||
resources: 'usable'
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to test field detection
|
||||
*/
|
||||
@@ -109,9 +82,40 @@ export const testField = (fieldName: FormField, elementId: string, htmlFile: str
|
||||
/**
|
||||
* Test the birthdate format.
|
||||
*/
|
||||
export const testBirthdateFormat = (expectedFormat: string, htmlFile: string) : void => {
|
||||
export const testBirthdateFormat = (expectedFormat: string, htmlFile: string, focusedElementId: string) : void => {
|
||||
it('should detect correct birthdate format', () => {
|
||||
const { result } = setupFormTest(htmlFile);
|
||||
const { result } = setupFormTest(htmlFile, focusedElementId);
|
||||
expect(result.birthdateField.format).toBe(expectedFormat);
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Load a test HTML file.
|
||||
*/
|
||||
const loadTestHtml = (filename: string): string => {
|
||||
return readFileSync(join(__dirname, 'test-forms', filename), 'utf-8');
|
||||
};
|
||||
|
||||
/**
|
||||
* Set up a form detection test.
|
||||
*/
|
||||
const setupFormTest = (htmlFile: string, focusedElementId: string) : { document: Document, result: LoginForm } => {
|
||||
const html = loadTestHtml(htmlFile);
|
||||
const dom = new JSDOM(html, {
|
||||
url: 'http://localhost',
|
||||
runScripts: 'dangerously',
|
||||
resources: 'usable'
|
||||
});
|
||||
const document = dom.window.document;
|
||||
|
||||
// Set focus on specified element if provided
|
||||
let focusedElement = document.getElementById(focusedElementId);
|
||||
if (!focusedElement) {
|
||||
throw new Error(`Focus element with id "${focusedElementId}" not found in test HTML`);
|
||||
}
|
||||
|
||||
// Create a new form detector with the focused element.
|
||||
const formDetector = new FormDetector(document, focusedElement);
|
||||
const result = formDetector.detectForms()[0];
|
||||
return { document, result };
|
||||
};
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<main class="css-qg5f0b"><header><h1 class="css-1tfp4pj">Create your account</h1></header><section class="css-t9ru4u"><form method="post" action="/register?componentEventParams=componentType%3Didentityauthentication%26componentId%3Dguardian_signin_header&returnUrl" data-testid="main-form" class="css-1pw6os9"><div id="recaptcha" class="g-recaptcha css-wyp7vc"><div class="grecaptcha-badge" data-style="bottomright" style="width: 256px; height: 60px; display: block; transition: right 0.3s; position: fixed; bottom: 14px; right: -186px; box-shadow: gray 0px 0px 5px; border-radius: 2px; overflow: hidden;"><div class="grecaptcha-logo"></div><div class="grecaptcha-error"></div><textarea id="g-recaptcha-response" name="g-recaptcha-response" class="g-recaptcha-response" style="width: 250px; height: 40px; border: 1px solid rgb(193, 193, 193); margin: 10px 25px; padding: 0px; resize: none; display: none;"></textarea></div></div><input type="hidden" name="_csrf" value="JQefOI4bd9Rd91s8TaOBEacBoqnZ56ht_sgm6T6wOMSdu0hx0eluksj9fviOzBzmQUcBotU4NKcfGxuQ2cVIfw"><input type="hidden" name="_csrfPageUrl"><input type="hidden" name="ref" value="https://profile.theguardian.com/register/email"><input type="hidden" name="refViewId" value="m6qgqwadrjxge8ow8pnc"><div><label for="P0-0" class="css-0"><div class="css-1daa38r">Email </div></label><div style="position: relative; display: inline-block; width: 100%;"><input type="email" id="P0-0" aria-required="true" aria-invalid="false" aria-describedby="" required="" name="email" autocomplete="off" class="css-l9vrwo" style="width: 428px; display: inline-block;"><div class="aliasvault-input-icon" style="
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 8px;
|
||||
margin: auto;
|
||||
cursor: pointer;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
z-index: 999999;
|
||||
">
|
||||
<img src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCA1MDAgNTAwIiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCA1MDAgNTAwIiB4bWw6c3BhY2U9InByZXNlcnZlIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8cGF0aCBkPSJtNDU5Ljg3IDI5NC45NWMwLjAxNjIwNSA1LjQwMDUgMC4wMzI0MSAxMC44MDEtMC4zNTAyMiAxNi44NzMtMS4xMTEgNi4zMzkyLTEuMTk0MSAxMi4xNzMtMi42MzUxIDE3LjY0OS0xMC45MjIgNDEuNTA4LTM2LjczMSA2OS40ODEtNzcuMzUxIDgzLjQwOC03LjIxNTcgMi40NzM5LTE0Ljk3MiAzLjM3MDItMjIuNDc5IDQuOTk1LTIzLjYyOSAwLjA0MjIwNS00Ny4yNTcgMC4xMTQ1My03MC44ODYgMC4xMjAyNy00Ni43NjIgMC4wMTEzMjItOTMuNTIzLTAuMDE0MTYtMTQwLjk1LTAuNDM0MTEtOC41OS0yLjAwMjQtMTYuNzY2LTIuODM1Mi0yNC4zOTgtNS4zMzI2LTIxLjU5NS03LjA2NjYtMzkuNTIzLTE5LjY1Ni01My43MDgtMzcuNTUyLTEwLjIyNy0xMi45MDMtMTcuNTc5LTI3LjE3LTIxLjI4LTQzLjIyMS0xLjQ3NS02LjM5NjctMi40NzExLTEyLjkwNC0zLjY4NTItMTkuMzYxLTAuMDUxODQ5LTUuNzQ3LTAuMTAzNy0xMS40OTQgMC4yNjkxNS0xNy44ODYgNC4xNTktNDIuOTczIDI3LjY4LTcxLjYzOCA2My41NjItOTIuMTUzIDAtMC43MDc2MS0wLjAwMTk2MS0xLjY5ODggMy4xMmUtNCAtMi42OSAwLjAyMjQ4NC05LjgyOTMtMS4zMDcxLTE5Ljg5NCAwLjM1NjY0LTI5LjQzOCAzLjIzOTEtMTguNTc5IDExLjA4LTM1LjI3MiAyMy43NjMtNDkuNzczIDEyLjA5OC0xMy44MzIgMjYuNDU3LTIzLjk4OSA0My42MDktMzAuMDI5IDcuODEzLTIuNzUxMiAxNi4xNC00LjA0MTcgMjQuMjM0LTUuOTk0OCA3LjM5Mi0wLjAyNTczNCAxNC43ODQtMC4wNTE0NiAyMi44MzUgMC4zMjI1MyA0LjE5NTkgMC45NTM5MiA3Ljc5NDYgMS4yNTM4IDExLjI1OCAyLjEwNTMgMTcuMTYgNC4yMTkyIDMyLjI4NyAxMi4xNzYgNDUuNDY5IDI0LjEwNCAyLjI1NTggMi4wNDExIDQuMzcyIDYuNjI0MSA5LjYyMSAzLjg2OCAxNi44MzktOC44NDE5IDM0LjcxOC0xMS41OTcgNTMuNjAzLTguNTk0IDE2Ljc5MSAyLjY2OTkgMzEuNjAyIDkuNDMwOCA0NC4yMzYgMjAuNjM2IDExLjUzMSAxMC4yMjcgMTkuODQgMjIuODQxIDI1LjM5MyAzNy4yMzYgNi4zNDM2IDE2LjQ0NSAxMC4zODkgMzMuMTYzIDYuMDc5OCA0OS4zODkgNy45NTg3IDguOTMyMSAxNS44MDcgMTYuNzA0IDIyLjQyMSAyNS40MTQgOS4xNjIgMTIuMDY1IDE1LjMzIDI1Ljc0NiAxOC4xNDQgNDAuNzc2IDAuOTcwNDYgNS4xODQ4IDEuOTExMSAxMC4zNzUgMi44NjU0IDE1LjU2M20tNzEuNTk3IDcxLjAxMmM1LjU2MTUtNS4yMjg0IDEyLjAwMi05Ljc5ODYgMTYuNTA4LTE1LjgxNyAxMC40NzQtMTMuOTkyIDE0LjMzMy0yOS45MTYgMTEuMjg4LTQ3LjQ0Ni0yLjI0OTYtMTIuOTUtOC4xOTczLTI0LjA3Ni0xNy4yNDMtMzMuMDYzLTEyLjc0Ni0xMi42NjMtMjguODY1LTE4LjYxNC00Ni43ODYtMTguNTY5LTY5LjkxMiAwLjE3NzEyLTEzOS44MiAwLjU2ODMxLTIwOS43NCAwLjk2MTc2LTE1LjkyMiAwLjA4OTU5OS0yOS4xNjggNy40MjA5LTM5LjY4NSAxOC4yOTYtMTQuNDUgMTQuOTQ0LTIwLjQwOCAzMy4zNDMtMTYuNjU1IDU0LjM2OCAyLjI3NjMgMTIuNzU0IDguMjE2NyAyMy43NDggMTcuMTU4IDMyLjY2IDEzLjI5OSAxMy4yNTUgMzAuMDk3IDE4LjY1MyA0OC43MjggMTguNjUxIDU5LjMyMS0wLjAwNTE4OCAxMTguNjQgMC4wNDIzNTggMTc3Ljk2LTAuMDQ2NjAxIDkuNTkxMi0wLjAxNDM3NCAxOS4xODEtMC44NjU4OCAyOC43NzMtMC44ODg1NSAxMC42NDktMC4wMjUxNDYgMTkuOTc4LTMuODI1IDI5LjY4Ny05LjEwNzR6IiBmaWxsPSIjRUVDMTcwIi8+CjxwYXRoIGQ9Im0xNjIuNzcgMjkzYzE1LjY1NCA0LjM4ODMgMjAuNjI3IDIyLjk2NyAxMC4zMDQgMzQuOTgtNS4zMTA0IDYuMTc5NS0xNC44MTcgOC4zMjA4LTI0LjI3OCA1LjA0NzItNy4wNzIzLTIuNDQ3MS0xMi4zMzItMTAuMzYyLTEyLjg3Ni0xNy45MzMtMS4wNDUxLTE0LjU0MiAxMS4wODktMjMuMTc2IDIxLjcwNS0yMy4wNDYgMS41Nzk0IDAuMDE5Mjg3IDMuMTUxNyAwLjYxNTY2IDUuMTQ2MSAwLjk1MTg0eiIgZmlsbD0iI0VFQzE3MCIvPgo8cGF0aCBkPSJtMjI3LjE4IDI5My42NGM3Ljg0OTkgMi4zOTczIDExLjkzOCA4LjIxNDMgMTMuNTI0IDE1LjA3NyAxLjg1OTEgOC4wNDM5LTAuNDQ4MTcgMTUuNzA2LTcuMTU4OCAyMS4xMjEtNi43NjMzIDUuNDU3Mi0xNC40MTcgNi44Nzk0LTIyLjU3OCAzLjE0ODMtOC4yOTcyLTMuNzkzMy0xMi44MzYtMTAuODQ5LTEyLjczNi0xOS40MzggMC4xNjg3LTE0LjQ5NyAxNC4xMy0yNS4zNjggMjguOTQ4LTE5LjkwOHoiIGZpbGw9IiNFRUMxNzAiLz4KPHBhdGggZD0ibTI2MS41NyAzMTkuMDdjLTIuNDk1LTE0LjQxOCA0LjY4NTMtMjIuNjAzIDE0LjU5Ni0yNi4xMDggOS44OTQ1LTMuNDk5NSAyMy4xODEgMy40MzAzIDI2LjI2NyAxMy43NzkgNC42NTA0IDE1LjU5MS03LjE2NTEgMjkuMDY0LTIxLjY2NSAyOC4xNjEtOC41MjU0LTAuNTMwODgtMTcuMjAyLTYuNTA5NC0xOS4xOTgtMTUuODMxeiIgZmlsbD0iI0VFQzE3MCIvPgo8cGF0aCBkPSJtMzM2LjkxIDMzMy40MWMtOS4wMTc1LTQuMjQ5MS0xNS4zMzctMTQuMzQ5LTEzLjgyOS0yMS42ODIgMy4wODI1LTE0Ljk4OSAxMy4zNDEtMjAuMzA0IDIzLjAxOC0xOS41ODUgMTAuNjUzIDAuNzkxNDEgMTcuOTMgNy40MDcgMTkuNzY1IDE3LjU0NyAxLjk1ODggMTAuODI0LTQuMTE3MSAxOS45MzktMTMuNDk0IDIzLjcwMy01LjI3MiAyLjExNjItMTAuMDkxIDEuNTA4Ni0xNS40NiAwLjAxNzg4M3oiIGZpbGw9IiNFRUMxNzAiLz4KPC9zdmc+" style="width: 100%; height: 100%;">
|
||||
</div></div></div><div class="css-sh1xpo"><label id="6031_description" class="css-141740"><div class="css-wqo2kt"><span class="css-1yorkrx">Saturday Edition newsletter</span><span class="css-o3hxza">An exclusive email highlighting the week’s best Guardian journalism from the editor-in-chief, Katharine Viner.</span></div><input name="6031" type="checkbox" role="switch" checked="" aria-labelledby="6031_description" class="css-1ksnltd"><span aria-hidden="true" aria-labelledby="6031_description" class="css-14xm7ey"></span></label><label id="similar_guardian_products_description" class="css-141740"><div class="css-wqo2kt"><span class="css-o3hxza">Receive information on our products and ways to support and enjoy our journalism. Toggle to opt out.</span></div><input name="similar_guardian_products" type="checkbox" role="switch" checked="" aria-labelledby="similar_guardian_products_description" class="css-1ksnltd"><span aria-hidden="true" aria-labelledby="similar_guardian_products_description" class="css-14xm7ey"></span></label></div><div class="css-gsjv6q"><div class="css-h996v6">Newsletters may contain information about Guardian products, services and chosen charities or online advertisements.</div><div class="css-h996v6">This service is protected by reCAPTCHA and the Google <a href="https://policies.google.com/privacy" rel="noopener noreferrer" class="css-1ss633q">privacy policy</a> and <a href="https://policies.google.com/terms" rel="noopener noreferrer" class="css-1ss633q">terms of service</a> apply.</div></div><button type="submit" aria-live="polite" data-cy="main-form-submit-button" aria-disabled="false" class="css-1aiqjx3">Next</button></form><hr class="css-5bcdcv"><p class="css-jckhu">Already have an account? <a href="/signin?componentEventParams=componentType%3Didentityauthentication%26componentId%3Dguardian_signin_header&returnUrl=https%3A%2F%2Fwww.theguardian.com%2Feurope" class="css-1ss633q">Sign in</a></p></section></main>
|
||||
@@ -1,4 +1,4 @@
|
||||
<main class="css-qg5f0b"><header><h1 class="css-1tfp4pj">Create your account</h1></header><section class="css-t9ru4u"><form method="post" action="/register?componentEventParams=componentType%3Didentityauthentication%26componentId%3Dguardian_signin_header&returnUrl" data-testid="main-form" class="css-1pw6os9"><div id="recaptcha" class="g-recaptcha css-wyp7vc"><div class="grecaptcha-badge" data-style="bottomright" style="width: 256px; height: 60px; display: block; transition: right 0.3s; position: fixed; bottom: 14px; right: -186px; box-shadow: gray 0px 0px 5px; border-radius: 2px; overflow: hidden;"><div class="grecaptcha-logo"></div><div class="grecaptcha-error"></div><textarea id="g-recaptcha-response" name="g-recaptcha-response" class="g-recaptcha-response" style="width: 250px; height: 40px; border: 1px solid rgb(193, 193, 193); margin: 10px 25px; padding: 0px; resize: none; display: none;"></textarea></div></div><input type="hidden" name="_csrf" value="JQefOI4bd9Rd91s8TaOBEacBoqnZ56ht_sgm6T6wOMSdu0hx0eluksj9fviOzBzmQUcBotU4NKcfGxuQ2cVIfw"><input type="hidden" name="_csrfPageUrl"><input type="hidden" name="ref" value="https://profile.theguardian.com/register/email"><input type="hidden" name="refViewId" value="m6qgqwadrjxge8ow8pnc"><div><label for="P0-0" class="css-0"><div class="css-1daa38r">Email </div></label><div style="position: relative; display: inline-block; width: 100%;"><input type="email" id="P0-0" aria-required="true" aria-invalid="false" aria-describedby="" required="" name="email" autocomplete="off" class="css-l9vrwo" style="width: 428px; display: inline-block;"><div class="aliasvault-input-icon" style="
|
||||
<main class="css-qg5f0b"><header><h1 class="css-1tfp4pj">Create your account</h1></header><section class="css-t9ru4u"><form method="post" action="/register?componentEventParams=componentType%3Didentityauthentication%26componentId%3Dguardian_signin_header&returnUrl" data-testid="main-form" class="css-1pw6os9"><div id="recaptcha" class="g-recaptcha css-wyp7vc"><div class="grecaptcha-badge" data-style="bottomright" style="width: 256px; height: 60px; display: block; transition: right 0.3s; position: fixed; bottom: 14px; right: -186px; box-shadow: gray 0px 0px 5px; border-radius: 2px; overflow: hidden;"><div class="grecaptcha-logo"></div><div class="grecaptcha-error"></div><textarea id="g-recaptcha-response" name="g-recaptcha-response" class="g-recaptcha-response" style="width: 250px; height: 40px; border: 1px solid rgb(193, 193, 193); margin: 10px 25px; padding: 0px; resize: none; display: none;"></textarea></div></div><input type="hidden" name="_csrf" value="JQefOI4bd9Rd91s8TaOBEacBoqnZ56ht_sgm6T6wOMSdu0hx0eluksj9fviOzBzmQUcBotU4NKcfGxuQ2cVIfw"><input type="hidden" name="_csrfPageUrl"><input type="hidden" name="ref" value="https://profile.theguardian.com/register/email"><input type="hidden" name="refViewId" value="m6qgqwadrjxge8ow8pnc"><div><label for="P0-0" class="css-0"><div class="css-1daa38r">Email </div></label><div style="position: relative; display: inline-block; width: 100%;"><input type="email" id="P0-0" aria-required="true" aria-invalid="false" aria-describedby="" required="" name="email" class="css-l9vrwo" style="width: 428px; display: inline-block;"><div class="aliasvault-input-icon" style="
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<!--
|
||||
This is a form with only a search input that should not be detected as a login/registration form.
|
||||
-->
|
||||
<div id="center" class="style-scope ytd-masthead">
|
||||
|
||||
<yt-searchbox role="search" client-ve-type="10349" class="ytSearchboxComponentHost ytSearchboxComponentDesktop ytd-masthead ytSearchboxComponentHostDark">
|
||||
<div class="ytSearchboxComponentInputBox ytSearchboxComponentInputBoxDark"><form action="/results" class="ytSearchboxComponentSearchForm"><input name="search_query" aria-controls="i0" aria-expanded="true" type="text" autocomplete="off" autocorrect="off" aria-autocomplete="list" role="combobox" class="ytSearchboxComponentInput yt-searchbox-input title" placeholder="Search"></form></div><button aria-label="Search" class="ytSearchboxComponentSearchButton ytSearchboxComponentSearchButtonDark" title="Search"><yt-icon><!--css-build:shady--><!--css_build_scope:yt-icon--><!--css_build_styles:video.youtube.src.web.polymer.shared.ui.styles.yt_base_styles.yt.base.styles.css.js,video.youtube.src.web.polymer.shared.core.yt_icon.yt.icon.css.js--><span class="yt-icon-shape style-scope yt-icon yt-spec-icon-shape"><div style="width: 100%; height: 100%; display: block; fill: currentcolor;"><svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" height="24" viewBox="0 0 24 24" width="24" focusable="false" aria-hidden="true" style="pointer-events: none; display: inherit; width: 100%; height: 100%;"><path clip-rule="evenodd" d="M16.296 16.996a8 8 0 11.707-.708l3.909 3.91-.707.707-3.909-3.909zM18 11a7 7 0 00-14 0 7 7 0 1014 0z" fill-rule="evenodd"></path></svg></div></span></yt-icon></button><div id="i0" role="listbox" hidden="true" class="ytSearchboxComponentSuggestionsContainer ytSearchboxComponentSuggestionsContainerDark" style="min-width: 536px;"></div></yt-searchbox>
|
||||
<dom-if class="style-scope ytd-masthead"><template is="dom-if"></template></dom-if>
|
||||
<ytd-searchbox id="search" class="style-scope ytd-masthead" hidden="" system-icons="" role="search"><!--css-build:shady--><!--css_build_scope:ytd-searchbox--><!--css_build_styles:video.youtube.src.web.polymer.shared.ui.styles.yt_base_styles.yt.base.styles.css.js--><form id="search-form" action="/results" class="style-scope ytd-searchbox">
|
||||
<div id="container" class="style-scope ytd-searchbox">
|
||||
<yt-icon id="search-icon" class="style-scope ytd-searchbox"><!--css-build:shady--><!--css_build_scope:yt-icon--><!--css_build_styles:video.youtube.src.web.polymer.shared.ui.styles.yt_base_styles.yt.base.styles.css.js,video.youtube.src.web.polymer.shared.core.yt_icon.yt.icon.css.js--><span class="yt-icon-shape style-scope yt-icon yt-spec-icon-shape"><div style="width: 100%; height: 100%; display: block; fill: currentcolor;"><svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" height="24" viewBox="0 0 24 24" width="24" focusable="false" aria-hidden="true" style="pointer-events: none; display: inherit; width: 100%; height: 100%;"><path clip-rule="evenodd" d="M16.296 16.996a8 8 0 11.707-.708l3.909 3.91-.707.707-3.909-3.909zM18 11a7 7 0 00-14 0 7 7 0 1014 0z" fill-rule="evenodd"></path></svg></div></span></yt-icon>
|
||||
<div id="search-input" class="ytd-searchbox-spt" slot="search-input"><input id="search" autocapitalize="none" autocomplete="off" autocorrect="off" name="search_query" tabindex="0" type="text" spellcheck="false" placeholder="Search" aria-label="Search"></div>
|
||||
<div id="search-clear-button" class="style-scope ytd-searchbox" hidden=""><ytd-button-renderer class="style-scope ytd-searchbox" button-renderer="" button-next=""><!--css-build:shady--><yt-button-shape><button class="yt-spec-button-shape-next yt-spec-button-shape-next--text yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m yt-spec-button-shape-next--icon-only-default" aria-disabled="false" aria-label="Clear search query" title="" style=""><div class="yt-spec-button-shape-next__icon" aria-hidden="true"><yt-icon style="width: 24px; height: 24px;"><!--css-build:shady--><!--css_build_scope:yt-icon--><!--css_build_styles:video.youtube.src.web.polymer.shared.ui.styles.yt_base_styles.yt.base.styles.css.js,video.youtube.src.web.polymer.shared.core.yt_icon.yt.icon.css.js--><span class="yt-icon-shape style-scope yt-icon yt-spec-icon-shape"><div style="width: 100%; height: 100%; display: block; fill: currentcolor;"><svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24" viewBox="0 0 24 24" width="24" focusable="false" aria-hidden="true" style="pointer-events: none; display: inherit; width: 100%; height: 100%;"><path d="m12.71 12 8.15 8.15-.71.71L12 12.71l-8.15 8.15-.71-.71L11.29 12 3.15 3.85l.71-.71L12 11.29l8.15-8.15.71.71L12.71 12z"></path></svg></div></span></yt-icon></div><yt-touch-feedback-shape style="border-radius: inherit;"><div class="yt-spec-touch-feedback-shape yt-spec-touch-feedback-shape--touch-response" aria-hidden="true"><div class="yt-spec-touch-feedback-shape__stroke" style=""></div><div class="yt-spec-touch-feedback-shape__fill" style=""></div></div></yt-touch-feedback-shape></button></yt-button-shape><tp-yt-paper-tooltip offset="8" disable-upgrade=""></tp-yt-paper-tooltip></ytd-button-renderer></div>
|
||||
</div>
|
||||
<div id="search-container" class="ytd-searchbox-spt" slot="search-container"></div>
|
||||
</form>
|
||||
<button id="search-icon-legacy" class="style-scope ytd-searchbox" aria-label="Search">
|
||||
<yt-icon class="style-scope ytd-searchbox"><!--css-build:shady--><!--css_build_scope:yt-icon--><!--css_build_styles:video.youtube.src.web.polymer.shared.ui.styles.yt_base_styles.yt.base.styles.css.js,video.youtube.src.web.polymer.shared.core.yt_icon.yt.icon.css.js--><span class="yt-icon-shape style-scope yt-icon yt-spec-icon-shape"><div style="width: 100%; height: 100%; display: block; fill: currentcolor;"><svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" height="24" viewBox="0 0 24 24" width="24" focusable="false" aria-hidden="true" style="pointer-events: none; display: inherit; width: 100%; height: 100%;"><path clip-rule="evenodd" d="M16.296 16.996a8 8 0 11.707-.708l3.909 3.91-.707.707-3.909-3.909zM18 11a7 7 0 00-14 0 7 7 0 1014 0z" fill-rule="evenodd"></path></svg></div></span></yt-icon>
|
||||
<tp-yt-paper-tooltip prefix="" class="style-scope ytd-searchbox" role="tooltip" tabindex="-1" aria-label="tooltip"><!--css-build:shady--><!--css_build_scope:tp-yt-paper-tooltip--><!--css_build_styles:video.youtube.src.web.polymer.shared.ui.styles.yt_base_styles.yt.base.styles.css.js,third_party.javascript.youtube_components.tp_yt_paper_tooltip.tp.yt.paper.tooltip.css.js--><div id="tooltip" class="hidden style-scope tp-yt-paper-tooltip" style-target="tooltip">
|
||||
Search
|
||||
</div>
|
||||
</tp-yt-paper-tooltip>
|
||||
</button>
|
||||
</ytd-searchbox>
|
||||
<yt-icon-button id="search-button-narrow" class="style-scope ytd-masthead" role="button"><!--css-build:shady--><!--css_build_scope:yt-icon-button--><!--css_build_styles:video.youtube.src.web.polymer.shared.ui.styles.yt_base_styles.yt.base.styles.css.js,video.youtube.src.web.polymer.shared.ui.yt_icon_button.yt.icon.button.css.js--><button id="button" class="style-scope yt-icon-button" aria-label="Search">
|
||||
<yt-icon class="topbar-icons style-scope ytd-masthead" icon="yt-icons:search" disable-upgrade="">
|
||||
</yt-icon>
|
||||
<tp-yt-paper-tooltip for="search-button-narrow" class="style-scope ytd-masthead" disable-upgrade="" hidden="">
|
||||
Search
|
||||
</tp-yt-paper-tooltip>
|
||||
</button><yt-interaction id="interaction" class="circular style-scope yt-icon-button"><!--css-build:shady--><!--css_build_scope:yt-interaction--><!--css_build_styles:video.youtube.src.web.polymer.shared.ui.styles.yt_base_styles.yt.base.styles.css.js,video.youtube.src.web.polymer.shared.ui.yt_interaction.yt.interaction.css.js--><div class="stroke style-scope yt-interaction"></div><div class="fill style-scope yt-interaction"></div></yt-interaction></yt-icon-button>
|
||||
<div id="voice-search-button" class="style-scope ytd-masthead">
|
||||
<ytd-button-renderer class="style-scope ytd-masthead" button-renderer="" button-next=""><!--css-build:shady--><yt-button-shape><button class="yt-spec-button-shape-next yt-spec-button-shape-next--text yt-spec-button-shape-next--overlay yt-spec-button-shape-next--size-m yt-spec-button-shape-next--icon-only-default" aria-disabled="false" aria-label="Search with your voice" title="" style=""><div class="yt-spec-button-shape-next__icon" aria-hidden="true"><yt-icon style="width: 24px; height: 24px;"><!--css-build:shady--><!--css_build_scope:yt-icon--><!--css_build_styles:video.youtube.src.web.polymer.shared.ui.styles.yt_base_styles.yt.base.styles.css.js,video.youtube.src.web.polymer.shared.core.yt_icon.yt.icon.css.js--><span class="yt-icon-shape style-scope yt-icon yt-spec-icon-shape"><div style="width: 100%; height: 100%; display: block; fill: currentcolor;"><svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24" focusable="false" aria-hidden="true" style="pointer-events: none; display: inherit; width: 100%; height: 100%;"><path d="M12 3c-1.66 0-3 1.37-3 3.07v5.86c0 1.7 1.34 3.07 3 3.07s3-1.37 3-3.07V6.07C15 4.37 13.66 3 12 3zm6.5 9h-1c0 3.03-2.47 5.5-5.5 5.5S6.5 15.03 6.5 12h-1c0 3.24 2.39 5.93 5.5 6.41V21h2v-2.59c3.11-.48 5.5-3.17 5.5-6.41z"></path></svg></div></span></yt-icon></div><yt-touch-feedback-shape style="border-radius: inherit;"><div class="yt-spec-touch-feedback-shape yt-spec-touch-feedback-shape--overlay-touch-response" aria-hidden="true"><div class="yt-spec-touch-feedback-shape__stroke" style=""></div><div class="yt-spec-touch-feedback-shape__fill" style=""></div></div></yt-touch-feedback-shape></button></yt-button-shape><tp-yt-paper-tooltip offset="8" role="tooltip" tabindex="-1" aria-label="tooltip"><!--css-build:shady--><!--css_build_scope:tp-yt-paper-tooltip--><!--css_build_styles:video.youtube.src.web.polymer.shared.ui.styles.yt_base_styles.yt.base.styles.css.js,third_party.javascript.youtube_components.tp_yt_paper_tooltip.tp.yt.paper.tooltip.css.js--><div id="tooltip" class="hidden style-scope tp-yt-paper-tooltip" style-target="tooltip">
|
||||
Search with your voice
|
||||
</div>
|
||||
</tp-yt-paper-tooltip></ytd-button-renderer></div>
|
||||
</div>
|
||||
@@ -0,0 +1,117 @@
|
||||
<!--
|
||||
This is a form with only a dropdown that should not be detected as a login/registration form.
|
||||
-->
|
||||
<div class="d-flex flex-justify-between mb-md-3 flex-column-reverse flex-md-row flex-items-end">
|
||||
<div class="d-flex flex-justify-start flex-auto my-4 my-md-0 width-full width-md-auto" role="search">
|
||||
<details class="details-reset details-overlay subnav-search-context flex-shrink-0" id="filters-select-menu">
|
||||
<summary data-view-component="true" class="rounded-right-0 color-border-emphasis Button--secondary Button--medium Button" aria-haspopup="menu" role="button"> <span class="Button-content">
|
||||
<span class="Button-label">Filters</span>
|
||||
</span>
|
||||
<span class="Button-visual Button-trailingAction">
|
||||
<svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true" class="octicon octicon-triangle-down">
|
||||
<path d="m4.427 7.427 3.396 3.396a.25.25 0 0 0 .354 0l3.396-3.396A.25.25 0 0 0 11.396 7H4.604a.25.25 0 0 0-.177.427Z"></path>
|
||||
</svg>
|
||||
</span>
|
||||
</summary>
|
||||
<details-menu class="SelectMenu" role="menu">
|
||||
<div class="SelectMenu-modal">
|
||||
<div class="SelectMenu-header">
|
||||
<h3 class="SelectMenu-title">Filter Issues</h3>
|
||||
<button class="SelectMenu-closeButton" type="button" data-toggle-for="filters-select-menu">
|
||||
<svg aria-label="Close menu" aria-hidden="false" role="img" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true" class="octicon octicon-x">
|
||||
<path d="M3.72 3.72a.75.75 0 0 1 1.06 0L8 6.94l3.22-3.22a.749.749 0 0 1 1.275.326.749.749 0 0 1-.215.734L9.06 8l3.22 3.22a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215L8 9.06l-3.22 3.22a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042L6.94 8 3.72 4.78a.75.75 0 0 1 0-1.06Z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="SelectMenu-list" data-pjax="#repo-content-pjax-container" data-turbo-frame="repo-content-turbo-frame">
|
||||
<a class="SelectMenu-item" role="menuitemradio" aria-checked="false" href="/lanedirt/AliasVault/issues?q=is%3Aopen" data-ga-click="Pull Requests, Search filter, Open issues and pull requests" data-turbo-frame="repo-content-turbo-frame">
|
||||
<svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true" class="octicon octicon-check SelectMenu-icon SelectMenu-icon--check">
|
||||
<path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"></path>
|
||||
</svg>
|
||||
Open issues and pull requests
|
||||
</a>
|
||||
<a class="SelectMenu-item" role="menuitemradio" aria-checked="false" href="/lanedirt/AliasVault/issues?q=is%3Aopen+is%3Aissue+author%3A%40me" data-ga-click="Pull Requests, Search filter, Your issues" data-turbo-frame="repo-content-turbo-frame">
|
||||
<svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true" class="octicon octicon-check SelectMenu-icon SelectMenu-icon--check">
|
||||
<path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"></path>
|
||||
</svg>
|
||||
Your issues
|
||||
</a>
|
||||
<a class="SelectMenu-item" role="menuitemradio" aria-checked="false" href="/lanedirt/AliasVault/issues?q=is%3Aopen+is%3Apr+author%3A%40me" data-ga-click="Pull Requests, Search filter, Your pull requests" data-turbo-frame="repo-content-turbo-frame">
|
||||
<svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true" class="octicon octicon-check SelectMenu-icon SelectMenu-icon--check">
|
||||
<path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"></path>
|
||||
</svg>
|
||||
Your pull requests
|
||||
</a>
|
||||
<a class="SelectMenu-item" role="menuitemradio" aria-checked="false" href="/lanedirt/AliasVault/issues?q=is%3Aopen+assignee%3A%40me" data-ga-click="Pull Requests, Search filter, Everything assigned to you" data-turbo-frame="repo-content-turbo-frame">
|
||||
<svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true" class="octicon octicon-check SelectMenu-icon SelectMenu-icon--check">
|
||||
<path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"></path>
|
||||
</svg>
|
||||
Everything assigned to you
|
||||
</a>
|
||||
<a class="SelectMenu-item" role="menuitemradio" aria-checked="false" href="/lanedirt/AliasVault/issues?q=is%3Aopen+mentions%3A%40me" data-ga-click="Pull Requests, Search filter, Everything mentioning you" data-turbo-frame="repo-content-turbo-frame">
|
||||
<svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true" class="octicon octicon-check SelectMenu-icon SelectMenu-icon--check">
|
||||
<path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"></path>
|
||||
</svg>
|
||||
Everything mentioning you
|
||||
</a>
|
||||
<a class="SelectMenu-item" role="menuitemradio" href="#" target="_blank" rel="noopener noreferrer" data-turbo-frame="repo-content-turbo-frame">
|
||||
<svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true" class="octicon octicon-link-external mr-2">
|
||||
<path d="M3.75 2h3.5a.75.75 0 0 1 0 1.5h-3.5a.25.25 0 0 0-.25.25v8.5c0 .138.112.25.25.25h8.5a.25.25 0 0 0 .25-.25v-3.5a.75.75 0 0 1 1.5 0v3.5A1.75 1.75 0 0 1 12.25 14h-8.5A1.75 1.75 0 0 1 2 12.25v-8.5C2 2.784 2.784 2 3.75 2Zm6.854-1h4.146a.25.25 0 0 1 .25.25v4.146a.25.25 0 0 1-.427.177L13.03 4.03 9.28 7.78a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042l3.75-3.75-1.543-1.543A.25.25 0 0 1 10.604 1Z"></path>
|
||||
</svg>
|
||||
<strong>View advanced search syntax</strong>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</details-menu>
|
||||
</details>
|
||||
|
||||
<!-- '"` --><!-- </textarea></xmp> --><form class="subnav-search width-full d-flex " data-pjax="#repo-content-pjax-container" data-turbo-frame="repo-content-turbo-frame" role="search" aria-label="Issues" data-turbo="false" action="/lanedirt/AliasVault/pulls" accept-charset="UTF-8" method="get">
|
||||
<input type="text" name="q" id="js-issues-search" value="is:pr is:open " class="form-control subnav-search-input input-contrast width-full" placeholder="Search all issues" aria-label="Search all issues" data-hotkey="Control+/,Meta+/" autocomplete="false">
|
||||
<svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true" class="octicon octicon-search subnav-search-icon">
|
||||
<path d="M10.68 11.74a6 6 0 0 1-7.922-8.982 6 6 0 0 1 8.982 7.922l3.04 3.04a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215ZM11.5 7a4.499 4.499 0 1 0-8.997 0A4.499 4.499 0 0 0 11.5 7Z"></path>
|
||||
</svg>
|
||||
</form> <div class="ml-2 pl-2 d-none d-md-flex">
|
||||
|
||||
<nav class="subnav-links float-left d-flex no-wrap" aria-label="Issue">
|
||||
<a selected_link="repo_pulls" class="js-selected-navigation-item subnav-item" data-selected-links="repo_labels /lanedirt/AliasVault/labels" href="/lanedirt/AliasVault/labels">
|
||||
<svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true" class="octicon octicon-tag">
|
||||
<path d="M1 7.775V2.75C1 1.784 1.784 1 2.75 1h5.025c.464 0 .91.184 1.238.513l6.25 6.25a1.75 1.75 0 0 1 0 2.474l-5.026 5.026a1.75 1.75 0 0 1-2.474 0l-6.25-6.25A1.752 1.752 0 0 1 1 7.775Zm1.5 0c0 .066.026.13.073.177l6.25 6.25a.25.25 0 0 0 .354 0l5.025-5.025a.25.25 0 0 0 0-.354l-6.25-6.25a.25.25 0 0 0-.177-.073H2.75a.25.25 0 0 0-.25.25ZM6 5a1 1 0 1 1 0 2 1 1 0 0 1 0-2Z"></path>
|
||||
</svg>
|
||||
Labels
|
||||
<span title="12" data-view-component="true" class="Counter d-none d-md-inline">12</span>
|
||||
</a> <a selected_link="repo_pulls" class="js-selected-navigation-item subnav-item" data-selected-links="repo_milestones /lanedirt/AliasVault/milestones" href="/lanedirt/AliasVault/milestones">
|
||||
<svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true" class="octicon octicon-milestone">
|
||||
<path d="M7.75 0a.75.75 0 0 1 .75.75V3h3.634c.414 0 .814.147 1.13.414l2.07 1.75a1.75 1.75 0 0 1 0 2.672l-2.07 1.75a1.75 1.75 0 0 1-1.13.414H8.5v5.25a.75.75 0 0 1-1.5 0V10H2.75A1.75 1.75 0 0 1 1 8.25v-3.5C1 3.784 1.784 3 2.75 3H7V.75A.75.75 0 0 1 7.75 0Zm4.384 8.5a.25.25 0 0 0 .161-.06l2.07-1.75a.248.248 0 0 0 0-.38l-2.07-1.75a.25.25 0 0 0-.161-.06H2.75a.25.25 0 0 0-.25.25v3.5c0 .138.112.25.25.25h9.384Z"></path>
|
||||
</svg>
|
||||
Milestones
|
||||
<span title="0" data-view-component="true" class="Counter d-none d-md-inline">0</span>
|
||||
</a></nav>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-3 d-flex flex-justify-between width-full width-md-auto" data-pjax="">
|
||||
<span class="d-md-none">
|
||||
|
||||
<nav class="subnav-links float-left d-flex no-wrap" aria-label="Issue">
|
||||
<a selected_link="repo_pulls" class="js-selected-navigation-item subnav-item" data-selected-links="repo_labels /lanedirt/AliasVault/labels" href="/lanedirt/AliasVault/labels">
|
||||
<svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true" class="octicon octicon-tag">
|
||||
<path d="M1 7.775V2.75C1 1.784 1.784 1 2.75 1h5.025c.464 0 .91.184 1.238.513l6.25 6.25a1.75 1.75 0 0 1 0 2.474l-5.026 5.026a1.75 1.75 0 0 1-2.474 0l-6.25-6.25A1.752 1.752 0 0 1 1 7.775Zm1.5 0c0 .066.026.13.073.177l6.25 6.25a.25.25 0 0 0 .354 0l5.025-5.025a.25.25 0 0 0 0-.354l-6.25-6.25a.25.25 0 0 0-.177-.073H2.75a.25.25 0 0 0-.25.25ZM6 5a1 1 0 1 1 0 2 1 1 0 0 1 0-2Z"></path>
|
||||
</svg>
|
||||
Labels
|
||||
<span title="12" data-view-component="true" class="Counter d-none d-md-inline">12</span>
|
||||
</a> <a selected_link="repo_pulls" class="js-selected-navigation-item subnav-item" data-selected-links="repo_milestones /lanedirt/AliasVault/milestones" href="/lanedirt/AliasVault/milestones">
|
||||
<svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true" class="octicon octicon-milestone">
|
||||
<path d="M7.75 0a.75.75 0 0 1 .75.75V3h3.634c.414 0 .814.147 1.13.414l2.07 1.75a1.75 1.75 0 0 1 0 2.672l-2.07 1.75a1.75 1.75 0 0 1-1.13.414H8.5v5.25a.75.75 0 0 1-1.5 0V10H2.75A1.75 1.75 0 0 1 1 8.25v-3.5C1 3.784 1.784 3 2.75 3H7V.75A.75.75 0 0 1 7.75 0Zm4.384 8.5a.25.25 0 0 0 .161-.06l2.07-1.75a.248.248 0 0 0 0-.38l-2.07-1.75a.25.25 0 0 0-.161-.06H2.75a.25.25 0 0 0-.25.25v3.5c0 .138.112.25.25.25h9.384Z"></path>
|
||||
</svg>
|
||||
Milestones
|
||||
<span title="0" data-view-component="true" class="Counter d-none d-md-inline">0</span>
|
||||
</a></nav>
|
||||
|
||||
</span>
|
||||
<a href="/lanedirt/AliasVault/compare" data-hotkey="c" data-ga-click="Repository, go to compare view, location:pull request list; text:New pull request" tabindex="0" data-view-component="true" class="Button--primary Button--medium Button"> <span class="Button-content">
|
||||
<span class="Button-label"><span class="d-none d-md-block">New pull request</span>
|
||||
<span class="d-block d-md-none">New</span></span>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user