etherpad-lite/src/templates/pad.html
John McLear 7a1d9519f6
fix(a11y): skip-to-content link + hide line numbers from screen readers (#7255) (#7758)
* fix(a11y): skip-to-content link + hide line numbers from AT (#7255)

PR #7451 landed editor-region labelling but didn't address two
remaining screen-reader complaints from the original report:

1. No way to bypass the toolbar. Murphy noted that screen-reader
   users had to swipe through ~18 toolbar buttons to reach the
   editor (WCAG 2.4.1 Bypass Blocks). Adds a skip link as the
   first child of <body>, hidden offscreen until keyboard focus
   reveals it. Click handler in pad.ts routes through ace_focus
   so the inner contenteditable actually receives focus — a plain
   href="#editorcontainer" anchor only scrolls.

2. Line numbers read individually as "1, 2, 3, ...". The sidediv
   is visual scaffolding for sighted users; it carries no useful
   information for AT. Adds aria-hidden="true" on creation in
   ace.ts so screen readers skip it entirely.

Also fixes two issues found while implementing this:

- The Escape/Alt+F9 hint that PR #7451 added in the inner iframe's
  body was being wiped by Ace2Inner.init() (line splices manage
  body children). Move it to the inner <head> — aria-describedby
  resolves by ID anywhere in the same document. Localize the text
  via html10n.get('pad.editor.keyboardHint').

- Skip link and keyboard hint text are both routed through new
  locale keys (pad.editor.skipToContent, pad.editor.keyboardHint)
  so they translate via the existing html10n pipeline.

Adds two Playwright assertions for the new behavior.

Refs #7255

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(a11y): remove editor auto-focus so skip link is Tab-reachable

PR #7758 added a skip-to-content link as the first body child, but on
local testing it was unreachable via Tab from the URL bar — the
standard WCAG 2.4.1 entry path. Root cause: postAceInit calls
padeditor.ace.focus() on load, which moves focus into the editor
iframe. Tab inside the editor inserts an indent character (handled by
ace2_inner's key handler) rather than bubbling to the parent page, so
the skip link sat in the DOM but no user could ever reach it.

Drop the load-time auto-focus. Initial focus is now on <body>, and the
first Tab focuses #skip-to-content as expected. Users now click or Tab
into the editor; existing Escape → toolbar exit (PR #7451) and the
new Enter-on-skip-link path both still work.

Cost: the long-standing "start typing immediately on a fresh pad" UX
goes away — one extra click to start editing. This matches the modern
accessible default for rich text editors (Slack, Discord, Google Docs
all require an explicit focus before accepting input) and removes a
keyboard trap that violated WCAG 2.1.2 even after PR #7451's Escape
escape hatch.

Adds a Playwright assertion that fresh-page Tab focuses the skip link.

Refs #7255

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test: focus the editor before typing in three specs

These three Playwright tests started failing after the autofocus removal
in ef95498 because they reached for keyboard input without first
clicking into the editor:

- enter.spec.ts: `lastLine.focus()` is a no-op on a <div> (not natively
  focusable), so Enter went to <body> instead of the contenteditable.
  Replaced with `.click()`.
- indentation.spec.ts: `selectText()` triple-clicks but the focus event
  didn't reliably reach the inner contenteditable in CI; added an
  explicit `.click()` first.
- collab_client.spec.ts: each test opens its own browser contexts via
  goToPad, which does not click into the editor; selectText() in
  replaceLineText was the only thing landing focus, and that proved
  flaky. Added `body.click()` at the top of replaceLineText.

All three previously relied on the page-load auto-focus that ef95498
removed. The new pattern (click first, then type) is what writeToPad
and clearPadContent already do — these specs now match.

Refs #7255

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(a11y): move skip link above eejs body block so plugins can't outrank it

The WITH_PLUGINS Playwright suite caught a regression in the previous
commit: ep_set_title_on_pad's eejsBlock_body hook prepends a
<div id='pad_title'> that contains a focusable <a href="">Loading...</a>
into the body block, putting that anchor ahead of #skip-to-content in
DOM tab order. First Tab landed on the title link instead of the skip
link, breaking the WCAG 2.4.1 entry path.

Moving the skip link before <% e.begin_block("body"); %> takes it out
of the plugin-modifiable region — eejsBlock_body hooks can prepend to
the block content but cannot reach above the block's own start tag.
Verified locally with ep_set_title_on_pad installed:

  On load:     BODY
  After Tab 1: A#skip-to-content
  After Tab 2: A (pad_title's Loading link — plugin content)

Refs #7255

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 19:23:52 +01:00

648 lines
34 KiB
HTML

<%
var langs = require("ep_etherpad-lite/node/hooks/i18n").availableLangs
, pluginUtils = require('ep_etherpad-lite/static/js/pluginfw/shared')
, skinColors = require('ep_etherpad-lite/node/utils/SkinColors')
;
var renderLang = (req && typeof req.acceptsLanguages === 'function'
&& req.acceptsLanguages(Object.keys(langs))) || 'en';
var renderDir = (langs[renderLang] && langs[renderLang].direction === 'rtl') ? 'rtl' : 'ltr';
// theme-color matches the configured toolbar so mobile address bars don't
// paint a mismatched system color above the toolbar on first paint. We do
// not emit a prefers-color-scheme: dark variant: the client-side dark-mode
// auto-switch is gated on enableDarkMode, matchMedia, and a localStorage
// white-mode override, none of which a media query can express.
var configuredColor = skinColors.configuredToolbarColor(settings.skinName, settings.skinVariants);
%>
<!doctype html>
<html lang="<%=renderLang%>" dir="<%=renderDir%>" translate="no" class="pad <%=pluginUtils.clientPluginNames().join(' '); %> <%=settings.skinVariants%>">
<head>
<% e.begin_block("htmlHead"); %>
<% e.end_block(); %>
<title><%=settings.title%></title>
<%- typeof socialMetaHtml !== 'undefined' ? socialMetaHtml : '' %>
<link rel="manifest" href="../../manifest.json" />
<script>
/*
|@licstart The following is the entire license notice for the
JavaScript code in this page.|
Copyright 2011 Peter Martischka, Primary Technology.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
|@licend The above is the entire license notice
for the JavaScript code in this page.|
*/
</script>
<meta charset="utf-8">
<meta name="robots" content="noindex, nofollow">
<meta name="referrer" content="no-referrer">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0">
<% if (configuredColor) { %><meta name="theme-color" content="<%=configuredColor%>"><% } %>
<link rel="shortcut icon" href="../favicon.ico">
<% e.begin_block("styles"); %>
<link href="../static/css/pad.css?v=<%=settings.randomVersionString%>" rel="stylesheet">
<% e.begin_block("customStyles"); %>
<link href="../static/skins/<%=encodeURI(settings.skinName)%>/pad.css?v=<%=settings.randomVersionString%>" rel="stylesheet">
<% e.end_block(); %>
<style title="dynamicsyntax"></style>
<% e.end_block(); %>
<link rel="localizations" type="application/l10n+json" href="../locales.json" />
</head>
<body>
<!-- WCAG 2.4.1 — bypass the toolbar and jump straight to the editor.
Visible only when keyboard-focused; click handler in pad.ts routes
focus into the editor iframe rather than just scrolling.
MUST live outside the `body` eejs block: plugins like
ep_set_title_on_pad use eejsBlock_body to prepend a focusable
title bar, which would otherwise come before this link in DOM
order and steal the first Tab. -->
<a id="skip-to-content" class="skip-link" href="#editorcontainer"
data-l10n-id="pad.editor.skipToContent">Skip to editor</a>
<% e.begin_block("body"); %>
<!----------------------------->
<!--------- TOOLBAR ----------->
<!----------------------------->
<div id="editbar" class="toolbar">
<div id="toolbar-overlay"></div>
<ul class="menu_left" role="toolbar">
<% e.begin_block("editbarMenuLeft"); %>
<%- toolbar.menu(settings.toolbar.left, isReadOnly, 'left', 'pad') %>
<% e.end_block(); %>
</ul>
<!-- HISTORY-MODE CONTROLS — slider + play/step buttons swap into the
toolbar's left zone while scrubbing pad history. The formatting
menu_left is hidden in history mode so this fills its space. The
outer slider is just a remote control for the embedded
timeslider iframe's BroadcastSlider; pad_mode.ts owns the
bridging.
Accessible names + tooltip titles are populated by pad_mode.ts
after html10n is ready, reusing the existing `timeslider.*`
translation keys. We deliberately do NOT put data-l10n-id on
the icon buttons — that would set their textContent, which on
a buttonicon glyph would draw the localized string on screen. -->
<div id="history-controls" class="history-controls" role="toolbar" hidden>
<button type="button" id="history-playpause" class="buttonicon buttonicon-play"
aria-pressed="false"></button>
<button type="button" id="history-leftstep" class="buttonicon buttonicon-step-backward">
</button>
<button type="button" id="history-rightstep" class="buttonicon buttonicon-step-forward">
</button>
<input type="range" id="history-slider-input" class="history-slider-input"
min="0" max="0" value="0" step="1">
<span id="history-timer" class="history-timer hide-for-mobile" aria-live="polite"></span>
<!-- Follow toggle: an icon-only button. The eye is always rendered;
the diagonal slash overlay is shown only when aria-pressed is
false. Backed by a hidden checkbox so existing pad_mode.ts
bridge code (which reads .checked) keeps working. -->
<input type="checkbox" id="history-options-followContents" class="sr-only" checked>
<label for="history-options-followContents" class="history-follow-toggle hide-for-mobile"
aria-hidden="true">
<svg class="history-follow-eye" width="16" height="16" viewBox="0 0 16 16"
aria-hidden="true" focusable="false">
<path class="history-follow-eye-shape"
d="M1.2 8C2.7 4.4 5.1 2.5 8 2.5s5.3 1.9 6.8 5.5c-1.5 3.6-3.9 5.5-6.8 5.5S2.7 11.6 1.2 8z"
fill="none" stroke="currentColor" stroke-width="1.4"/>
<circle class="history-follow-eye-pupil" cx="8" cy="8" r="2.3" fill="currentColor"/>
<line class="history-follow-eye-slash" x1="2" y1="14" x2="14" y2="2"
stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>
</svg>
</label>
<select id="history-playbackspeed" class="history-speed hide-for-mobile"
aria-label="Playback speed"
data-l10n-aria-label="true">
<option value="100" data-l10n-id="timeslider.settings.playbackSpeed.original">Original speed</option>
<option value="realtime" data-l10n-id="timeslider.settings.playbackSpeed.realtime">Realtime</option>
<option value="200" data-l10n-id="timeslider.settings.playbackSpeed.200ms">200 ms</option>
<option value="500" data-l10n-id="timeslider.settings.playbackSpeed.500ms">500 ms</option>
<option value="1000" data-l10n-id="timeslider.settings.playbackSpeed.1000ms">1000 ms</option>
</select>
</div>
<ul class="menu_right" role="toolbar">
<% e.begin_block("editbarMenuRight"); %>
<%- toolbar.menu(settings.toolbar.right, isReadOnly, 'right', 'pad') %>
<% e.end_block(); %>
</ul>
<button type="button" class="show-more-icon-btn" aria-label="Show more toolbar buttons" aria-expanded="false"></button> <!-- use on small screen to display hidden toolbar buttons -->
</div>
<% e.begin_block("afterEditbar"); %><% e.end_block(); %>
<!--------------------------------------------------------------->
<!-- HISTORY-MODE BANNER (visible only when scrubbing history) -->
<!--------------------------------------------------------------->
<div id="history-banner" class="history-banner" role="status" aria-live="polite" hidden>
<span class="history-banner-label" data-l10n-id="pad.historyMode.banner">Viewing history</span>
<span class="history-banner-rev" id="history-banner-rev"></span>
<span class="history-banner-date" id="history-banner-date"></span>
<button type="button" id="history-banner-return" class="btn btn-primary"
data-l10n-id="pad.historyMode.return">Return to live</button>
</div>
<div id="editorcontainerbox" class="flex-layout">
<% e.begin_block("editorContainerBox"); %>
<!----------------------------->
<!--- PAD EDITOR (in iframe) -->
<!----------------------------->
<div id="editorcontainer" class="editorcontainer" aria-label="Document editor"></div>
<!--------------------------------------------------------------------->
<!-- HISTORY-MODE IFRAME (mounted only while scrubbing pad history) -->
<!--------------------------------------------------------------------->
<div id="history-frame-mount" class="history-frame-mount" hidden></div>
<div id="editorloadingbox">
<% e.begin_block("permissionDenied"); %>
<div id="permissionDenied">
<p data-l10n-id="pad.permissionDenied" class="editorloadingbox-message">
You do not have permission to access this pad
</p>
</div>
<% e.end_block(); %>
<% e.begin_block("loading"); %>
<p data-l10n-id="pad.loading" id="loading" class="editorloadingbox-message">
<img src='../static/img/brand.svg' class='etherpadBrand'><br/>
Loading...
</p>
<% e.end_block(); %>
<noscript>
<p class="editorloadingbox-message">
<strong>
Sorry, you have to enable Javascript in order to use this.
</strong>
</p>
</noscript>
</div>
<!------------------------------------------------------------->
<!-- SETTINGS POPUP (change font, language, chat parameters) -->
<!------------------------------------------------------------->
<div id="settings" class="popup" role="dialog" aria-modal="true" aria-labelledby="settings-title"><div class="popup-content">
<h1 id="settings-title" data-l10n-id="pad.settings.title">Settings</h1>
<div class="settings-sections">
<div id="user-settings-section" class="settings-section">
<% e.begin_block("mySettings"); %>
<% if (settings.enablePadWideSettings) { %>
<h2 data-l10n-id="pad.settings.userSettings"></h2>
<% } else { %>
<h2 data-l10n-id="pad.settings.myView"></h2>
<% } %>
<p class="hide-for-mobile">
<input type="checkbox" id="options-disablechat">
<label for="options-disablechat" data-l10n-id="pad.settings.disablechat">Disable Chat</label>
</p>
<p class="hide-for-mobile">
<input type="checkbox" id="options-stickychat">
<label for="options-stickychat" data-l10n-id="pad.settings.stickychat"></label>
</p>
<p class="hide-for-mobile">
<input type="checkbox" id="options-chatandusers">
<label for="options-chatandusers" data-l10n-id="pad.settings.chatandusers"></label>
</p>
<p>
<input type="checkbox" id="options-colorscheck">
<label for="options-colorscheck" data-l10n-id="pad.settings.colorcheck"></label>
</p>
<p>
<input type="checkbox" id="options-fadeauthorcheck" checked>
<label for="options-fadeauthorcheck" data-l10n-id="pad.settings.fadeInactiveAuthorColors"></label>
</p>
<p>
<input type="checkbox" id="options-linenoscheck" checked>
<label for="options-linenoscheck" data-l10n-id="pad.settings.linenocheck"></label>
</p>
<p>
<input type="checkbox" id="options-rtlcheck">
<label for="options-rtlcheck" data-l10n-id="pad.settings.rtlcheck"></label>
</p>
<p id="theme-toggle-row" hidden>
<input type="checkbox" id="options-darkmode">
<label for="options-darkmode" data-l10n-id="pad.settings.darkMode">Dark mode</label>
</p>
<% if (settings.enablePadWideSettings) { %><p id="enforce-settings-notice" class="settings-notice" hidden
data-l10n-id="pad.settings.enforcedNotice">
These settings are locked for you by this pad's creator. Ask the pad creator if you need them changed.
</p><% } %>
<% e.end_block(); %>
<div class="dropdowns-container">
<% e.begin_block("mySettings.dropdowns"); %>
<p class="dropdown-line">
<label for="viewfontmenu" data-l10n-id="pad.settings.fontType">Font type:</label>
<select id="viewfontmenu">
<option value="" data-l10n-id="pad.settings.fontType.normal">Normal</option>
<%= fonts = ["Quicksand", "Roboto", "Alegreya", "PlayfairDisplay", "Montserrat", "OpenDyslexic", "RobotoMono"] %>
<% for(var i=0; i < fonts.length; i++) { %>
<option value="<%=fonts[i]%>"><%=fonts[i]%></option>
<% } %>
</select>
</p>
<p class="dropdown-line">
<label for="languagemenu" data-l10n-id="pad.settings.language">Language:</label>
<select id="languagemenu">
<% for (lang in langs) { %>
<option value="<%=lang%>"><%=langs[lang].nativeName%></option>
<% } %>
</select>
</p>
<% e.end_block(); %>
</div>
</div>
<% if (settings.enablePadWideSettings) { %><div id="pad-settings-section" class="settings-section" hidden>
<% e.begin_block("padSettings"); %>
<h2 data-l10n-id="pad.settings.padSettings"></h2>
<p class="hide-for-mobile">
<input type="checkbox" id="padsettings-options-disablechat">
<label for="padsettings-options-disablechat" data-l10n-id="pad.settings.disablechat">Disable Chat</label>
</p>
<p class="hide-for-mobile">
<input type="checkbox" id="padsettings-options-stickychat">
<label for="padsettings-options-stickychat" data-l10n-id="pad.settings.stickychat"></label>
</p>
<p class="hide-for-mobile">
<input type="checkbox" id="padsettings-options-chatandusers">
<label for="padsettings-options-chatandusers" data-l10n-id="pad.settings.chatandusers"></label>
</p>
<p>
<input type="checkbox" id="padsettings-options-colorscheck">
<label for="padsettings-options-colorscheck" data-l10n-id="pad.settings.colorcheck"></label>
</p>
<p>
<input type="checkbox" id="padsettings-options-fadeauthorcheck" checked>
<label for="padsettings-options-fadeauthorcheck" data-l10n-id="pad.settings.fadeInactiveAuthorColors"></label>
</p>
<p>
<input type="checkbox" id="padsettings-options-linenoscheck" checked>
<label for="padsettings-options-linenoscheck" data-l10n-id="pad.settings.linenocheck"></label>
</p>
<p>
<input type="checkbox" id="padsettings-options-rtlcheck">
<label for="padsettings-options-rtlcheck" data-l10n-id="pad.settings.rtlcheck"></label>
</p>
<p>
<input type="checkbox" id="padsettings-enforcecheck">
<label for="padsettings-enforcecheck" data-l10n-id="pad.settings.enforceSettings">Enforce settings for other users</label>
</p>
<% e.end_block(); %>
<div class="dropdowns-container">
<% e.begin_block("padSettings.dropdowns"); %>
<p class="dropdown-line">
<label for="padsettings-viewfontmenu" data-l10n-id="pad.settings.fontType">Font type:</label>
<select id="padsettings-viewfontmenu">
<option value="" data-l10n-id="pad.settings.fontType.normal">Normal</option>
<% for(var i=0; i < fonts.length; i++) { %>
<option value="<%=fonts[i]%>"><%=fonts[i]%></option>
<% } %>
</select>
</p>
<p class="dropdown-line">
<label for="padsettings-languagemenu" data-l10n-id="pad.settings.language">Language:</label>
<select id="padsettings-languagemenu">
<% for (lang in langs) { %>
<option value="<%=lang%>"><%=langs[lang].nativeName%></option>
<% } %>
</select>
</p>
<% e.end_block(); %>
</div>
<button class="btn btn-danger" data-l10n-id="pad.settings.deletePad" id="delete-pad">Delete pad</button>
</div><% } %>
</div>
<% if (!settings.requireAuthentication) { %>
<details id="delete-pad-with-token">
<summary data-l10n-id="pad.deletionToken.deleteWithToken">Delete with token</summary>
<label for="delete-pad-token-input" data-l10n-id="pad.deletionToken.tokenFieldLabel">Pad deletion token</label>
<input type="password" id="delete-pad-token-input" autocomplete="off" spellcheck="false">
<button id="delete-pad-token-submit" type="button" class="btn btn-danger"
data-l10n-id="pad.settings.deletePad">Delete pad</button>
</details>
<% } %>
<h2 data-l10n-id="pad.settings.about">About</h2>
<span data-l10n-id="pad.settings.poweredBy">Powered by</span>
<a href="https://etherpad.org" target="_blank" referrerpolicy="no-referrer" rel="noopener">Etherpad</a>
<% if (settings.exposeVersion) { %>(commit <%= settings.gitVersion %>)<% } %> </div></div>
<!--------------------------------------------------->
<!-- PAD DELETION TOKEN MODAL (shown once on create) -->
<!--------------------------------------------------->
<div id="deletiontoken-modal" class="popup" role="dialog" aria-modal="true"
aria-labelledby="deletiontoken-modal-title" aria-describedby="deletiontoken-modal-body" hidden>
<div class="popup-content">
<h1 id="deletiontoken-modal-title" data-l10n-id="pad.deletionToken.modalTitle">Save your pad deletion token</h1>
<p id="deletiontoken-modal-body" data-l10n-id="pad.deletionToken.modalBody">
This token is the only way to delete this pad if you lose your browser session or switch device. Save it somewhere safe — it is shown here exactly once.
</p>
<div class="deletiontoken-row">
<label for="deletiontoken-value" class="sr-only"
data-l10n-id="pad.deletionToken.tokenValueLabel">Your pad deletion token (read-only)</label>
<input type="text" id="deletiontoken-value" readonly>
<button id="deletiontoken-copy" type="button" aria-live="polite"
data-l10n-id="pad.deletionToken.copy">Copy</button>
</div>
<button id="deletiontoken-ack" type="button" class="btn btn-primary"
data-l10n-id="pad.deletionToken.acknowledge">I've saved it</button>
</div></div>
<!------------------------->
<!-- IMPORT EXPORT POPUP -->
<!------------------------->
<div id="import_export" class="popup" role="dialog" aria-modal="true" aria-labelledby="importexport-title"><div class="popup-content">
<h1 id="importexport-title" data-l10n-id="pad.importExport.import_export"></h1>
<div class="acl-write">
<% e.begin_block("importColumn"); %>
<h2 data-l10n-id="pad.importExport.import"></h2>
<div class="importmessage" id="importmessagenoconverter" data-l10n-id="pad.importExport.noConverter.innerHTML" hidden></div>
<form id="importform" method="post" action="" target="importiframe" enctype="multipart/form-data">
<div class="importformdiv" id="importformfilediv">
<input type="file" name="file" size="10" id="importfileinput">
<div class="importmessage" id="importmessagefail"></div>
</div>
<div id="import"></div>
<div class="importmessage" id="importmessagesuccess" data-l10n-id="pad.importExport.importSuccessful"></div>
<div class="importformdiv" id="importformsubmitdiv">
<span class="nowrap">
<input type="submit" class="btn btn-primary" name="submit" value="Import Now" disabled="disabled" id="importsubmitinput">
<div alt="" id="importstatusball" class="loadingAnimation" align="top"></div>
</span>
</div>
</form>
<% e.end_block(); %>
</div>
<div id="exportColumn">
<h2 data-l10n-id="pad.importExport.export"></h2>
<% e.begin_block("exportColumn"); %>
<a id="exportetherpada" data-l10n-id="pad.importExport.exportetherpada.title" target="_blank" rel="noopener" class="exportlink">
<span class="exporttype buttonicon buttonicon-file-powerpoint" id="exportetherpad" data-l10n-id="pad.importExport.exportetherpad" aria-hidden="true"></span>
</a>
<a id="exporthtmla" data-l10n-id="pad.importExport.exporthtmla.title" target="_blank" rel="noopener" class="exportlink">
<span class="exporttype buttonicon buttonicon-file-code" id="exporthtml" data-l10n-id="pad.importExport.exporthtml" aria-hidden="true"></span>
</a>
<a id="exportplaina" data-l10n-id="pad.importExport.exportplaina.title" target="_blank" rel="noopener" class="exportlink">
<span class="exporttype buttonicon buttonicon-file" id="exportplain" data-l10n-id="pad.importExport.exportplain" aria-hidden="true"></span>
</a>
<a id="exportworda" data-l10n-id="pad.importExport.exportworda.title" target="_blank" rel="noopener" class="exportlink">
<span class="exporttype buttonicon buttonicon-file-word" id="exportword" data-l10n-id="pad.importExport.exportword" aria-hidden="true"></span>
</a>
<a id="exportpdfa" data-l10n-id="pad.importExport.exportpdfa.title" target="_blank" rel="noopener" class="exportlink">
<span class="exporttype buttonicon buttonicon-file-pdf" id="exportpdf" data-l10n-id="pad.importExport.exportpdf" aria-hidden="true"></span>
</a>
<a id="exportopena" data-l10n-id="pad.importExport.exportopena.title" target="_blank" rel="noopener" class="exportlink">
<span class="exporttype buttonicon buttonicon-file-alt" id="exportopen" data-l10n-id="pad.importExport.exportopen" aria-hidden="true"></span>
</a>
<% e.end_block(); %>
</div>
</div></div>
<!---------------------------------------------------->
<!-- CONNECTIVITY POPUP (when you get disconnected) -->
<!---------------------------------------------------->
<div id="connectivity" class="popup" role="dialog" aria-modal="true" aria-label="Connection status"><div class="popup-content">
<% e.begin_block("modals"); %>
<div class="connected visible">
<h2 data-l10n-id="pad.modals.connected"></h2>
</div>
<div class="reconnecting">
<h1 data-l10n-id="pad.modals.reconnecting"></h1>
<i class='buttonicon buttonicon-spin5 icon-spin'>
<img src='../static/img/brand.svg' class='etherpadBrand'><br/>
</i>
</div>
<div class="userdup">
<h1 data-l10n-id="pad.modals.userdup"></h1>
<h2 data-l10n-id="pad.modals.userdup.explanation"></h2>
<p id="defaulttext" data-l10n-id="pad.modals.userdup.advice"></p>
<button id="forcereconnect" class="btn btn-primary" data-l10n-id="pad.modals.forcereconnect"></button>
</div>
<div class="unauth">
<h1 data-l10n-id="pad.modals.unauth"></h1>
<p id="defaulttext" data-l10n-id="pad.modals.unauth.explanation"></p>
<button id="forcereconnect" class="btn btn-primary" data-l10n-id="pad.modals.forcereconnect"></button>
</div>
<div class="looping">
<h1 data-l10n-id="pad.modals.disconnected"></h1>
<h2 data-l10n-id="pad.modals.looping.explanation"></h2>
<p data-l10n-id="pad.modals.looping.cause"></p>
</div>
<div class="initsocketfail">
<h1 data-l10n-id="pad.modals.initsocketfail"></h1>
<h2 data-l10n-id="pad.modals.initsocketfail.explanation"></h2>
<p data-l10n-id="pad.modals.initsocketfail.cause"></p>
</div>
<div class="slowcommit with_reconnect_timer">
<h1 data-l10n-id="pad.modals.disconnected"></h1>
<h2 data-l10n-id="pad.modals.slowcommit.explanation"></h2>
<p id="defaulttext" data-l10n-id="pad.modals.slowcommit.cause"></p>
<button id="forcereconnect" class="btn btn-primary" data-l10n-id="pad.modals.forcereconnect"></button>
</div>
<div class="badChangeset with_reconnect_timer">
<h1 data-l10n-id="pad.modals.disconnected"></h1>
<h2 data-l10n-id="pad.modals.badChangeset.explanation"></h2>
<p id="defaulttext" data-l10n-id="pad.modals.badChangeset.cause"></p>
<button id="forcereconnect" class="btn btn-primary" data-l10n-id="pad.modals.forcereconnect"></button>
</div>
<div class="corruptPad">
<h1 data-l10n-id="pad.modals.disconnected"></h1>
<h2 data-l10n-id="pad.modals.corruptPad.explanation"></h2>
<p data-l10n-id="pad.modals.corruptPad.cause"></p>
</div>
<div class="deleted">
<h1 data-l10n-id="pad.modals.deleted"></h1>
<p data-l10n-id="pad.modals.deleted.explanation"></p>
</div>
<div class="rateLimited">
<h1 data-l10n-id="pad.modals.rateLimited"></h1>
<p data-l10n-id="pad.modals.rateLimited.explanation"></p>
</div>
<div class="rejected">
<h1 data-l10n-id="pad.modals.disconnected"></h1>
<h2 data-l10n-id="pad.modals.rejected.explanation"></h2>
<p data-l10n-id="pad.modals.rejected.cause"></p>
</div>
<div class="disconnected with_reconnect_timer">
<% e.begin_block("disconnected"); %>
<h1 data-l10n-id="pad.modals.disconnected"></h1>
<h2 data-l10n-id="pad.modals.disconnected.explanation"></h2>
<p id="defaulttext" data-l10n-id="pad.modals.disconnected.cause"></p>
<button id="forcereconnect" class="btn btn-primary" data-l10n-id="pad.modals.forcereconnect"></button>
<% e.end_block(); %>
</div>
<form id="reconnectform" method="post" action="/ep/pad/reconnect" accept-charset="UTF-8" style="display: none;">
<input type="hidden" class="padId" name="padId">
<input type="hidden" class="diagnosticInfo" name="diagnosticInfo">
<input type="hidden" class="missedChanges" name="missedChanges">
</form>
<% e.end_block(); %>
</div></div>
<!-------------------------------->
<!-- EMBED POPUP (Share, embed) -->
<!-------------------------------->
<div id="embed" class="popup" role="dialog" aria-modal="true" aria-labelledby="embed-title"><div class="popup-content">
<% e.begin_block("embedPopup"); %>
<h1 id="embed-title" data-l10n-id="pad.share"></h1>
<div id="embedreadonly" class="acl-write">
<input type="checkbox" id="readonlyinput">
<label for="readonlyinput" data-l10n-id="pad.share.readonly"></label>
</div>
<div id="linkcode">
<h2 data-l10n-id="pad.share.link"></h2>
<input id="linkinput" type="text" value="" onclick="this.select()">
</div>
<div id="embedcode">
<h2 data-l10n-id="pad.share.emebdcode"></h2>
<input id="embedinput" type="text" value="" onclick="this.select()">
</div>
<% e.end_block(); %>
</div></div>
<div class="sticky-container">
<!---------------------------------------------------------------------->
<!-- USERS POPUP (set username, color, see other users names & color) -->
<!---------------------------------------------------------------------->
<div id="users" class="popup" role="dialog" aria-modal="true" aria-label="Users on this pad"><div class="popup-content">
<% e.begin_block("userlist"); %>
<div id="connectionstatus"></div>
<div id="myuser">
<div id="mycolorpicker" class="popup" role="dialog" aria-modal="true" aria-label="Choose your author color"><div class="popup-content">
<div id="colorpicker"></div>
<div class="btn-container">
<button id="mycolorpickersave" data-l10n-id="pad.colorpicker.save" class="btn btn-primary"></button>
<button id="mycolorpickercancel" data-l10n-id="pad.colorpicker.cancel" class="btn btn-default"></button>
<span id="mycolorpickerpreview" class="myswatchboxhoverable"></span>
</div>
</div></div>
<div id="myswatchbox"><div id="myswatch"></div></div>
<div id="myusernameform">
<input type="text" id="myusernameedit" disabled="disabled" data-l10n-id="pad.userlist.entername">
</div>
</div>
<div id="otherusers" role="region" aria-live="polite" aria-label="Active users on this pad">
<table id="otheruserstable" cellspacing="0" cellpadding="0" border="0">
<tr><td></td></tr>
</table>
</div>
<div id="userlistbuttonarea"></div>
<% e.end_block(); %>
</div></div>
<!----------------------------->
<!----------- CHAT ------------>
<!----------------------------->
<button type="button" id="chaticon" class="visible" title="Chat (Alt C)" data-l10n-id="pad.chat.title">
<span id="chatlabel" data-l10n-id="pad.chat"></span>
<span class="buttonicon buttonicon-chat" aria-hidden="true"></span>
<span id="chatcounter" aria-label="Unread messages">0</span>
</button>
<div id="chatbox">
<div class="chat-content">
<div id="titlebar">
<h1 id ="titlelabel" data-l10n-id="pad.chat"></h1>
<button type="button" id="titlecross" class="hide-reduce-btn" aria-label="Close chat">_</button>
<button type="button" id="titlesticky" class="stick-to-screen-btn" data-l10n-id="pad.chat.stick.title">&#9608;</button>
</div>
<div id="chattext" class="thin-scrollbar" aria-live="polite" aria-relevant="additions removals text" role="log" aria-atomic="false">
<div alt="loading.." id="chatloadmessagesball" class="chatloadmessages loadingAnimation" align="top"></div>
<button id="chatloadmessagesbutton" class="chatloadmessages" data-l10n-id="pad.chat.loadmessages"></button>
</div>
<div id="chatinputbox">
<form>
<textarea id="chatinput" maxlength="999" data-l10n-id="pad.chat.writeMessage.placeholder"></textarea>
</form>
</div>
</div>
</div>
</div>
<!------------------------------------------------------------------>
<!-- SKIN VARIANTS BUILDER (Customize rendering, only for admins) -->
<!------------------------------------------------------------------>
<% if (settings.skinName == 'colibris') { %>
<div id="skin-variants" class="popup" role="dialog" aria-modal="true" aria-labelledby="skin-variants-title"><div class="popup-content">
<h1 id="skin-variants-title">Skin Builder</h1>
<div class="dropdowns-container">
<% containers = [ "toolbar", "background", "editor" ]; %>
<% for(var i=0; i < containers.length; i++) { %>
<p class="dropdown-line">
<label class="skin-variant-container"><%=containers[i]%></label>
<select class="skin-variant skin-variant-color" data-container="<%=containers[i]%>">
<option value="super-light">Super Light</option>
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="super-dark">Super Dark</option>
</select>
</p>
<% } %>
</div>
<p>
<input type="checkbox" id="skin-variant-full-width" class="skin-variant"/>
<label for="skin-variant-full-width">Full Width Editor</label>
</p>
<p>
<label>Result to copy in settings.json</label>
<input id="skin-variants-result" type="text" readonly class="disabled" />
</p>
</div></div>
<% } %>
<% e.end_block(); %>
<div id="version-badge" role="status" aria-live="polite" style="display:none"></div>
</div> <!-- End of #editorcontainerbox -->
<% e.end_block(); %>
<!----------------------------->
<!-------- JAVASCRIPT --------->
<!----------------------------->
<% e.begin_block("scripts"); %>
<script src="<%=entrypoint%>"></script>
<% e.begin_block("customScripts"); %>
<script type="text/javascript" src="../static/skins/<%=encodeURI(settings.skinName)%>/pad.js?v=<%=settings.randomVersionString%>"></script>
<% e.end_block(); %>
<div style="display:none"><a href="/javascript" data-jslicense="1">JavaScript license information</a></div>
<% e.end_block(); %>
</body>
</html>