aboutsummaryrefslogtreecommitdiff
path: root/haddock-api/resources/html/js-src
diff options
context:
space:
mode:
authorBen Gamari <ben@smart-cactus.org>2021-01-05 16:23:02 -0500
committerBen Gamari <ben@smart-cactus.org>2021-01-05 16:23:02 -0500
commit99f61534a470b84c424fde0835215de6a3b6d721 (patch)
tree7152e5a53fe1c18e6fd5044d5aa3168ab99c3cc6 /haddock-api/resources/html/js-src
parent3e29ec51498dfe092b228889343dc8370ec0e64b (diff)
parent1e56f63c3197e7ca1c1e506e083c2bad25d08793 (diff)
Merge commit '1e56f63c3197e7ca1c1e506e083c2bad25d08793' into ghc-9.0
Diffstat (limited to 'haddock-api/resources/html/js-src')
-rw-r--r--haddock-api/resources/html/js-src/details-helper.ts106
-rw-r--r--haddock-api/resources/html/js-src/details-helper.tsx464
-rw-r--r--haddock-api/resources/html/js-src/init.ts4
-rw-r--r--haddock-api/resources/html/js-src/style-menu.tsx177
4 files changed, 559 insertions, 192 deletions
diff --git a/haddock-api/resources/html/js-src/details-helper.ts b/haddock-api/resources/html/js-src/details-helper.ts
deleted file mode 100644
index f13ac905..00000000
--- a/haddock-api/resources/html/js-src/details-helper.ts
+++ /dev/null
@@ -1,106 +0,0 @@
-import {getCookie} from "./cookies";
-
-interface HTMLDetailsElement extends HTMLElement {
- open: boolean
-}
-
-interface DetailsInfo {
- element: HTMLDetailsElement
- openByDefault: boolean
- toggles: HTMLElement[]
-}
-
-// Global state
-const detailsRegistry: { [id: string]: DetailsInfo } = {};
-const toggled: { [id: string]: true } = {}; /* stores which <details> are not in their default state */
-
-function lookupDetailsRegistry(id: string): DetailsInfo {
- const info = detailsRegistry[id];
- if (info == undefined) { throw new Error(`could not find <details> element with id '${id}'`); }
- return info;
-}
-
-function onDetailsToggle(ev: Event) {
- const element = ev.target as HTMLDetailsElement;
- const id = element.id;
- const info = lookupDetailsRegistry(id);
- const isOpen = info.element.open;
- for (const toggle of info.toggles) {
- if (toggle.classList.contains('details-toggle-control')) {
- toggle.classList.add(isOpen ? 'collapser' : 'expander');
- toggle.classList.remove(isOpen ? 'expander' : 'collapser');
- }
- }
- if (element.open == info.openByDefault) {
- delete toggled[id];
- } else {
- toggled[id] = true;
- }
- rememberToggled();
-}
-
-function gatherDetailsElements() {
- const els: HTMLDetailsElement[] = Array.prototype.slice.call(document.getElementsByTagName('details'));
- for (const el of els) {
- if (typeof el.id == "string" && el.id.length > 0) {
- detailsRegistry[el.id] = {
- element: el,
- openByDefault: !!el.open,
- toggles: [] // added later
- };
- el.addEventListener('toggle', onDetailsToggle);
- }
- }
-}
-
-function toggleDetails(id: string) {
- const {element} = lookupDetailsRegistry(id);
- element.open = !element.open;
-}
-
-function rememberToggled() {
- const sections: string[] = Object.keys(toggled);
- // cookie specific to this page; don't use setCookie which sets path=/
- document.cookie = "toggled=" + encodeURIComponent(sections.join('+'));
-}
-
-function restoreToggled() {
- const cookie = getCookie("toggled");
- if (!cookie) { return; }
- const ids = cookie.split('+');
- for (const id of ids) {
- const info = detailsRegistry[id];
- toggled[id] = true;
- if (info) {
- info.element.open = !info.element.open;
- }
- }
-}
-
-function onToggleClick(ev: MouseEvent) {
- ev.preventDefault();
- const toggle = ev.currentTarget as HTMLElement;
- const id = toggle.getAttribute('data-details-id');
- if (!id) { throw new Error("element with class 'details-toggle' has no 'data-details-id' attribute!"); }
- toggleDetails(id);
-}
-
-function initCollapseToggles() {
- const toggles: HTMLElement[] = Array.prototype.slice.call(document.getElementsByClassName('details-toggle'));
- toggles.forEach(toggle => {
- const id = toggle.getAttribute('data-details-id');
- if (!id) { throw new Error("element with class 'details-toggle' has no 'data-details-id' attribute!"); }
- const info = lookupDetailsRegistry(id);
- info.toggles.push(toggle);
- toggle.addEventListener('click', onToggleClick);
- if (toggle.classList.contains('details-toggle-control')) {
- toggle.classList.add(info.element.open ? 'collapser' : 'expander');
- }
- });
-}
-
-export function init() {
- gatherDetailsElements();
- restoreToggled();
- initCollapseToggles();
-} \ No newline at end of file
diff --git a/haddock-api/resources/html/js-src/details-helper.tsx b/haddock-api/resources/html/js-src/details-helper.tsx
new file mode 100644
index 00000000..871b5417
--- /dev/null
+++ b/haddock-api/resources/html/js-src/details-helper.tsx
@@ -0,0 +1,464 @@
+// This file implements the UI and logic for collapsing and expanding
+// instance lists ("details").
+//
+// A configuration ('GlobalConfig') controlled by the UI is persisted
+// in local storage in the user's browser. The configuration includes:
+//
+// * a global default state ('defaultInstanceState') for all instance
+// lists. The possible values for the global default are "collapsed"
+// and "expanded".
+//
+// * a global boolean option ('rememberToggles') to remember which
+// specific instance lists are not in the default state (e.g. which
+// instance lists are expanded when the default is "collapsed").
+//
+// * a local / per-page record of which specific instance lists are
+// not in the default state, when the global option
+// ('rememberToggles') to remember this info is enabled.
+//
+// The UI consists of an Instances menu with buttons for expanding and
+// collapsing all instance lists in the current module, a checkbox for
+// setting the global default state, and a checkbox to enable
+// remembering which instance lists are not in the global default
+// state. Also, each instance list on each module page has buttons for
+// collapsing and expanding.
+//
+// The logic of the UI is as follows:
+//
+// * setting the global default state erases any record of which
+// specific instances are in the non-default state, and collapses or
+// expands all instance lists on the current page to be in the
+// global default state.
+//
+// * changing boolean option for remembering which specific instance
+// lists are not in the default state erases any existing record of
+// which instances are not in the default state across all pages,
+// and updates the record for the current page when the option is
+// set to true. No collapsing or expanding is done.
+//
+// * toggling the collapse/expand state of a specific instance list
+// causes the state of that specific instance list to be recorded in
+// the persisted configuration iff the new state of that specific
+// instance list is different from the global default state, and the
+// option to remember instance list states is enabled. There are two
+// ways to toggle the collapse/expand state of a specific instance,
+// by clicking its collapse/expand button, and by clicking the
+// "collapse all" or "expand all" button in the Instances menu.
+//
+// This file also implements an association between elements (with
+// class "details-toggle" and "details-toggle-control") that can be
+// clicked to expand/collapse <details> elements, and the details
+// elements themselves. Note that this covers both <details> elements
+// that list instances -- what the above explained UI and logic is
+// concerned with -- and details about individual instances themselves
+// -- which the above is not concerend with. The association includes
+// adding event listeners that change CSS classes back and forth
+// between "expander" and "collapser"; these classes determine whether
+// an element is adorned with a right arrow ("expander") or a down
+// arrow ("collapser"). I don't understand why we don't directly use
+// the the HTML <summary> element type to allow the <details> elements
+// to be directly clickable.
+import preact = require("preact");
+
+const { h, Component } = preact;
+
+enum DefaultState { Closed, Open }
+
+interface GlobalConfig {
+ defaultInstanceState: DefaultState
+ rememberToggles: boolean
+}
+
+// Hackage domain-wide config
+const globalConfig: GlobalConfig = {
+ defaultInstanceState: DefaultState.Open,
+ rememberToggles: true,
+};
+
+class PreferencesButton extends Component<any, any> {
+ render(props: { title: string, onClick: () => void }) {
+ function onClick(e: Event) {
+ e.preventDefault();
+ props.onClick();
+ }
+ return <li><a href="#" onClick={onClick}>{props.title}</a></li>;
+ }
+}
+
+function addPreferencesButton(action: () => void) {
+ const pageMenu = document.querySelector('#page-menu') as HTMLUListElement;
+ const dummy = document.createElement('li');
+ pageMenu.insertBefore(dummy, pageMenu.firstChild);
+ preact.render(<PreferencesButton onClick={action} title="Instances" />, pageMenu, dummy);
+}
+
+type PreferencesProps = {
+ showHideTrigger: (action: () => void) => void
+}
+
+type PreferencesState = {
+ isVisible: boolean
+}
+
+class Preferences extends Component<PreferencesProps, PreferencesState> {
+ componentWillMount() {
+ document.addEventListener('mousedown', this.hide.bind(this));
+
+ document.addEventListener('keydown', (e) => {
+ if (this.state.isVisible) {
+ if (e.key === 'Escape') {
+ this.hide();
+ }
+ }
+ })
+ }
+
+ hide() {
+ this.setState({ isVisible: false });
+ }
+
+ show() {
+ if (!this.state.isVisible) {
+ this.setState({ isVisible: true });
+ }
+ }
+
+ toggleVisibility() {
+ if (this.state.isVisible) {
+ this.hide();
+ } else {
+ this.show();
+ }
+ }
+
+ componentDidMount() {
+ this.props.showHideTrigger(this.toggleVisibility.bind(this));
+ }
+
+ render(props: PreferencesProps, state: PreferencesState) {
+ const stopPropagation = (e: Event) => { e.stopPropagation(); };
+
+ return <div id="preferences" class={state.isVisible ? '' : 'hidden'}>
+ <div id="preferences-menu" class="dropdown-menu" onMouseDown={stopPropagation}>
+ <PreferencesMenu />
+ </div>
+ </div>;
+ }
+}
+
+function storeGlobalConfig() {
+ const json = JSON.stringify(globalConfig);
+ try {
+ // https://developer.mozilla.org/en-US/docs/Web/API/Storage/setItem#Exceptions.
+ localStorage.setItem('global', json);
+ } catch (e) {}
+}
+
+var globalConfigLoaded: boolean = false;
+
+function loadGlobalConfig() {
+ if (globalConfigLoaded) { return; }
+ globalConfigLoaded = true;
+ const global = localStorage.getItem('global');
+ if (!global) { return; }
+ try {
+ const globalConfig_ = JSON.parse(global);
+ globalConfig.defaultInstanceState = globalConfig_.defaultInstanceState;
+ globalConfig.rememberToggles = globalConfig_.rememberToggles;
+ } catch(e) {
+ // Gracefully handle errors related to changed config format.
+ if (e instanceof SyntaxError || e instanceof TypeError) {
+ localStorage.removeItem('global');
+ } else {
+ throw e;
+ }
+ }
+}
+
+function setDefaultInstanceState(s: DefaultState) {
+ return (e: Event) => {
+ globalConfig.defaultInstanceState = s;
+ putInstanceListsInDefaultState();
+ storeGlobalConfig();
+ clearLocalStorage();
+ storeLocalConfig();
+ }
+}
+
+function setRememberToggles(e: Event) {
+ const checked: boolean = (e as any).target.checked;
+ globalConfig.rememberToggles = checked;
+ storeGlobalConfig();
+ clearLocalStorage();
+ storeLocalConfig();
+}
+
+// Click event consumer for "default collapse" instance menu check box.
+function defaultCollapseOnClick(e: Event) {
+ const us = document.getElementById('default-collapse-instances') as HTMLInputElement;
+ if (us !== null) {
+ if (us.checked) {
+ setDefaultInstanceState(DefaultState.Closed)(e);
+ } else {
+ setDefaultInstanceState(DefaultState.Open)(e);
+ }
+ }
+}
+
+// Instances menu.
+function PreferencesMenu() {
+ loadGlobalConfig();
+ return <div>
+ <div>
+ <button type="button"
+ onClick={expandAllInstances}>
+ Expand All Instances
+ </button>
+ <button type="button"
+ onClick={collapseAllInstances}>
+ Collapse All Instances
+ </button>
+ </div>
+ <div>
+ <input type="checkbox"
+ id="default-collapse-instances"
+ name="default-instance-state"
+ checked={globalConfig.defaultInstanceState===DefaultState.Closed}
+ onClick={defaultCollapseOnClick}></input>
+
+ <span>Collapse All Instances By Default</span>
+ </div>
+ <div>
+ <input type="checkbox"
+ id="remember-toggles"
+ name="remember-toggles"
+ checked={globalConfig.rememberToggles}
+ onClick={setRememberToggles}></input>
+ <label for="remember-toggles">Remember Manually Collapsed/Expanded Instances</label>
+ </div>
+ </div>;
+}
+
+interface HTMLDetailsElement extends HTMLElement {
+ open: boolean
+}
+
+interface DetailsInfo {
+ element: HTMLDetailsElement
+ // Here 'toggles' is the list of all elements of class
+ // 'details-toggle-control' that control toggling 'element'. I
+ // believe this list is always length zero or one.
+ toggles: HTMLElement[]
+}
+
+// Mapping from <details> elements to their info.
+const detailsRegistry: { [id: string]: DetailsInfo } = {};
+
+function lookupDetailsRegistry(id: string): DetailsInfo {
+ const info = detailsRegistry[id];
+ if (info == undefined) { throw new Error(`could not find <details> element with id '${id}'`); }
+ return info;
+}
+
+// Return true iff instance lists are open by default.
+function getDefaultOpenSetting(): boolean {
+ return globalConfig.defaultInstanceState == DefaultState.Open;
+}
+
+// Event handler for "toggle" events, which are triggered when a
+// <details> element's "open" property changes. We don't deal with
+// any config stuff here, because we only change configs in response
+// to mouse clicks. In contrast, for example, this event is triggred
+// automatically once for every <details> element when the user clicks
+// the "collapse all elements" button.
+function onToggleEvent(ev: Event) {
+ const element = ev.target as HTMLDetailsElement;
+ const id = element.id;
+ const info = lookupDetailsRegistry(id);
+ const isOpen = info.element.open;
+ // Update the CSS of the toggle element users can click on to toggle
+ // 'element'. The "collapser" and "expander" classes control what
+ // kind of arrow appears next to the 'toggle' element.
+ for (const toggle of info.toggles) {
+ if (toggle.classList.contains('details-toggle-control')) {
+ toggle.classList.add(isOpen ? 'collapser' : 'expander');
+ toggle.classList.remove(isOpen ? 'expander' : 'collapser');
+ }
+ }
+}
+
+function gatherDetailsElements() {
+ const els: HTMLDetailsElement[] = Array.prototype.slice.call(document.getElementsByTagName('details'));
+ for (const el of els) {
+ if (typeof el.id == "string" && el.id.length > 0) {
+ detailsRegistry[el.id] = {
+ element: el,
+ toggles: [] // Populated later by 'initCollapseToggles'.
+ };
+ el.addEventListener('toggle', onToggleEvent);
+ }
+ }
+}
+
+// Return the id of the <details> element that the given 'toggle'
+// element toggles.
+function getDataDetailsId(toggle: Element): string {
+ const id = toggle.getAttribute('data-details-id');
+ if (!id) { throw new Error("element with class " + toggle + " has no 'data-details-id' attribute!"); }
+ return id;
+}
+
+// Toggle the "open" state of a <details> element when that element's
+// toggle element is clicked.
+function toggleDetails(toggle: Element) {
+ const id = getDataDetailsId(toggle);
+ const {element} = lookupDetailsRegistry(id);
+ element.open = !element.open;
+}
+
+// Prefix for local keys used with local storage. Idea is that other
+// modules could also use local storage with a different prefix and we
+// wouldn't step on each other's toes.
+//
+// NOTE: we're using the browser's "local storage" API via the
+// 'localStorage' object to store both "local" (to the current Haddock
+// page) and "global" (across all Haddock pages) configuration. Be
+// aware of these two different uses of the term "local".
+const localStoragePrefix: string = "local-details-config:";
+
+// Local storage key for the current page.
+function localStorageKey(): string {
+ return localStoragePrefix + document.location.pathname;
+}
+
+// Clear all local storage related to instance list configs.
+function clearLocalStorage() {
+ const keysToDelete: string[] = [];
+ for (var i = 0; i < localStorage.length; ++i) {
+ const key = localStorage.key(i);
+ if (key !== null && key.startsWith(localStoragePrefix)) {
+ keysToDelete.push(key);
+ }
+ }
+ keysToDelete.forEach(key => {
+ localStorage.removeItem(key);
+ });
+}
+
+// Compute and save the set of instance list ids that aren't in the
+// default state.
+function storeLocalConfig() {
+ if (!globalConfig.rememberToggles) return;
+ const instanceListToggles: HTMLElement[] =
+ // Restrict to 'details-toggle' elements for "instances"
+ // *plural*. These are the toggles that control instance lists and
+ // not the list of methods for individual instances.
+ Array.prototype.slice.call(document.getElementsByClassName(
+ 'instances details-toggle details-toggle-control'));
+ const nonDefaultInstanceListIds: string[] = [];
+ instanceListToggles.forEach(toggle => {
+ const id = getDataDetailsId(toggle);
+ const details = document.getElementById(id) as HTMLDetailsElement;
+ if (details.open != getDefaultOpenSetting()) {
+ nonDefaultInstanceListIds.push(id);
+ }
+ });
+
+ const json = JSON.stringify(nonDefaultInstanceListIds);
+ try {
+ // https://developer.mozilla.org/en-US/docs/Web/API/Storage/setItem#Exceptions.
+ localStorage.setItem(localStorageKey(), json);
+ } catch (e) {}
+}
+
+function putInstanceListsInDefaultState() {
+ switch (globalConfig.defaultInstanceState) {
+ case DefaultState.Closed: _collapseAllInstances(true); break;
+ case DefaultState.Open: _collapseAllInstances(false); break;
+ default: break;
+ }
+}
+
+// Expand and collapse instance lists according to global and local
+// config.
+function restoreToggled() {
+ loadGlobalConfig();
+ putInstanceListsInDefaultState();
+ if (!globalConfig.rememberToggles) { return; }
+ const local = localStorage.getItem(localStorageKey());
+ if (!local) { return; }
+ try {
+ const nonDefaultInstanceListIds: string[] = JSON.parse(local);
+ nonDefaultInstanceListIds.forEach(id => {
+ const info = lookupDetailsRegistry(id);
+ info.element.open = ! getDefaultOpenSetting();
+ });
+ } catch(e) {
+ // Gracefully handle errors related to changed config format.
+ if (e instanceof SyntaxError || e instanceof TypeError) {
+ localStorage.removeItem(localStorageKey());
+ } else {
+ throw e;
+ }
+ }
+}
+
+// Handler for clicking on the "toggle" element that toggles the
+// <details> element with id given by the 'data-details-id' property
+// of the "toggle" element.
+function onToggleClick(ev: MouseEvent) {
+ ev.preventDefault();
+ const toggle = ev.currentTarget as HTMLElement;
+ toggleDetails(toggle);
+ storeLocalConfig();
+}
+
+// Set event handlers on elements responsible for expanding and
+// collapsing <details> elements.
+//
+// This applies to all 'details-toggle's, not just to to top-level
+// 'details-toggle's that control instance lists.
+function initCollapseToggles() {
+ const toggles: HTMLElement[] = Array.prototype.slice.call(document.getElementsByClassName('details-toggle'));
+ toggles.forEach(toggle => {
+ const id = getDataDetailsId(toggle);
+ const info = lookupDetailsRegistry(id);
+ info.toggles.push(toggle);
+ toggle.addEventListener('click', onToggleClick);
+ if (toggle.classList.contains('details-toggle-control')) {
+ toggle.classList.add(info.element.open ? 'collapser' : 'expander');
+ }
+ });
+}
+
+// Collapse or expand all instances.
+function _collapseAllInstances(collapse: boolean) {
+ const ilists = document.getElementsByClassName('subs instances');
+ [].forEach.call(ilists, function (ilist : Element) {
+ const toggleType = collapse ? 'collapser' : 'expander';
+ const toggle = ilist.getElementsByClassName('instances ' + toggleType)[0];
+ if (toggle) {
+ toggleDetails(toggle);
+ }
+ });
+}
+
+function collapseAllInstances() {
+ _collapseAllInstances(true);
+ storeLocalConfig();
+}
+
+function expandAllInstances() {
+ _collapseAllInstances(false);
+ storeLocalConfig();
+}
+
+export function init(showHide?: (action: () => void) => void) {
+ gatherDetailsElements();
+ initCollapseToggles();
+ restoreToggled();
+ preact.render(
+ <Preferences showHideTrigger={showHide || addPreferencesButton} />,
+ document.body
+ );
+}
diff --git a/haddock-api/resources/html/js-src/init.ts b/haddock-api/resources/html/js-src/init.ts
index 877874ae..1bfa8b3c 100644
--- a/haddock-api/resources/html/js-src/init.ts
+++ b/haddock-api/resources/html/js-src/init.ts
@@ -17,6 +17,6 @@ function onDomReady(callback: () => void) {
onDomReady(() => {
document.body.classList.add('js-enabled');
styleMenu.init();
- detailsHelper.init();
quickJump.init();
-}); \ No newline at end of file
+ detailsHelper.init();
+});
diff --git a/haddock-api/resources/html/js-src/style-menu.tsx b/haddock-api/resources/html/js-src/style-menu.tsx
index bab840ca..2eb8344e 100644
--- a/haddock-api/resources/html/js-src/style-menu.tsx
+++ b/haddock-api/resources/html/js-src/style-menu.tsx
@@ -4,91 +4,14 @@ import {getCookie, setCookie, clearCookie} from "./cookies";
import preact = require("preact");
const { h, Component } = preact;
-const rspace = /\s\s+/g,
- rtrim = /^\s+|\s+$/g;
-
-function spaced(s: string) { return (" " + s + " ").replace(rspace, " "); }
-function trim(s: string) { return s.replace(rtrim, ""); }
-
-function hasClass(elem: Element, value: string) {
- const className = spaced(elem.className || "");
- return className.indexOf( " " + value + " " ) >= 0;
-}
-
-function addClass(elem: Element, value: string) {
- const className = spaced(elem.className || "");
- if ( className.indexOf( " " + value + " " ) < 0 ) {
- elem.className = trim(className + " " + value);
- }
-}
-
-function removeClass(elem: Element, value: string) {
- let className = spaced(elem.className || "");
- className = className.replace(" " + value + " ", " ");
- elem.className = trim(className);
-}
-
-function toggleClass(elem: Element, valueOn: string, valueOff: string, bool?: boolean): boolean {
- if (bool == null) { bool = ! hasClass(elem, valueOn); }
- if (bool) {
- removeClass(elem, valueOff);
- addClass(elem, valueOn);
- }
- else {
- removeClass(elem, valueOn);
- addClass(elem, valueOff);
- }
- return bool;
-}
-
-function makeClassToggle(valueOn: string, valueOff: string): (elem: Element, bool?: boolean) => boolean {
- return function(elem, bool) {
- return toggleClass(elem, valueOn, valueOff, bool);
- }
-}
-
-const toggleShow = makeClassToggle("show", "hide");
+// Get all of the styles that are available
function styles(): HTMLLinkElement[] {
const es = Array.prototype.slice.call(document.getElementsByTagName("link"));
return es.filter((a: HTMLLinkElement) => a.rel.indexOf("style") != -1 && a.title);
}
-class StyleMenuButton extends Component<any, any> {
-
- render(props: { stys: string[] }) {
- function action() {
- styleMenu();
- return false;
- };
-
- return <li><div id='style-menu-holder' onClick={action}>
- <a href='#'>Style &#9662;</a>
- <ul id='style-menu' class='hide'>
- {props.stys.map((sty) => {
- function action() {
- setActiveStyleSheet(sty);
- return false;
- };
-
- return <li><a href='#' onClick={action}>{sty}</a></li>;
- })}
- </ul>
- </div></li>;
- }
-
-}
-
-function addStyleMenu() {
- const stys = styles().map((s) => s.title);
- if (stys.length > 1) {
- const pageMenu = document.querySelector('#page-menu') as HTMLUListElement;
- const dummy = document.createElement('li');
- pageMenu.appendChild(dummy);
- preact.render(<StyleMenuButton stys={stys} title="Style"/>, pageMenu, dummy);
- }
-}
-
+// Set a style (including setting the cookie)
function setActiveStyleSheet(title: string) {
const as = styles();
let found: null | HTMLLinkElement = null;
@@ -110,17 +33,103 @@ function setActiveStyleSheet(title: string) {
}
}
+// Reset the style based on the cookie
function resetStyle() {
const s = getCookie("haddock-style");
if (s) setActiveStyleSheet(s);
}
-function styleMenu(show?: boolean) {
- const m = document.getElementById('style-menu');
- if (m) toggleShow(m, show);
+class StylesButton extends Component<any, any> {
+ render(props: { title: string, onClick: () => void }) {
+ function onClick(e: Event) {
+ e.preventDefault();
+ props.onClick();
+ }
+ return <li><a href="#" onClick={onClick}>{props.title}</a></li>;
+ }
}
-export function init() {
- addStyleMenu();
+// Add the style menu button
+function addStyleMenu(stys: string[], action: () => void) {
+ if (stys.length > 1) {
+ const pageMenu = document.querySelector('#page-menu') as HTMLUListElement;
+ const dummy = document.createElement('li');
+ pageMenu.appendChild(dummy);
+ preact.render(<StylesButton onClick={action} title="Styles"/>, pageMenu, dummy);
+ }
+}
+
+type StyleProps = {
+ styles: string[]
+ showHideTrigger: (action: () => void) => void
+}
+
+type StyleState = {
+ isVisible: boolean
+}
+
+// Represents the full style dropdown
+class Styles extends Component<StyleProps, StyleState> {
+
+ componentWillMount() {
+ document.addEventListener('mousedown', this.hide.bind(this));
+
+ document.addEventListener('keydown', (e) => {
+ if (this.state.isVisible) {
+ if (e.key === 'Escape') {
+ this.hide();
+ }
+ }
+ })
+ }
+
+ hide() {
+ this.setState({ isVisible: false });
+ }
+
+ show() {
+ if (!this.state.isVisible) {
+ this.setState({ isVisible: true });
+ }
+ }
+
+ toggleVisibility() {
+ if (this.state.isVisible) {
+ this.hide();
+ } else {
+ this.show();
+ }
+ }
+
+ componentDidMount() {
+ this.props.showHideTrigger(this.toggleVisibility.bind(this));
+ }
+
+ render(props: StyleProps, state: StyleState) {
+ const stopPropagation = (e: Event) => { e.stopPropagation(); };
+
+ return <div id="style" class={state.isVisible ? '' : 'hidden'}>
+ <div id="style-menu" class="dropdown-menu" onMouseDown={stopPropagation}>
+ {
+ props.styles.map((sty) =>
+ <button type="button"
+ onClick={(e) => { this.hide(); setActiveStyleSheet(sty) }}>
+ {sty}
+ </button>
+ )
+ }
+ </div>
+ </div>;
+ }
+}
+
+
+export function init(showHide?: (action: () => void) => void) {
+ const stys = styles().map((s) => s.title);
+ const addStylesButton = (action: () => void) => addStyleMenu(stys, action)
resetStyle();
+ preact.render(
+ <Styles showHideTrigger={showHide || addStylesButton} styles={stys} />,
+ document.body
+ );
}