layout.ts 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. /**
  2. * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author David Sehnal <david.sehnal@gmail.com>
  5. * @author Alexander Rose <alexander.rose@weirdbyte.de>
  6. */
  7. import { ParamDefinition as PD } from '../mol-util/param-definition';
  8. import { StatefulPluginComponent } from '../mol-plugin-state/component';
  9. import { PluginCommands } from './commands';
  10. import { PluginContext } from './context';
  11. const regionStateOptions = [
  12. ['full', 'Full'],
  13. ['collapsed', 'Collapsed'],
  14. ['hidden', 'Hidden'],
  15. ] as const;
  16. const simpleRegionStateOptions = [
  17. ['full', 'Full'],
  18. ['hidden', 'Hidden'],
  19. ] as const;
  20. export type PluginLayoutControlsDisplay = 'outside' | 'portrait' | 'landscape' | 'reactive'
  21. export const PluginLayoutStateParams = {
  22. isExpanded: PD.Boolean(false),
  23. showControls: PD.Boolean(true),
  24. regionState: PD.Group({
  25. left: PD.Select('full', regionStateOptions),
  26. top: PD.Select('full', simpleRegionStateOptions),
  27. right: PD.Select('full', simpleRegionStateOptions),
  28. bottom: PD.Select('full', simpleRegionStateOptions),
  29. }),
  30. controlsDisplay: PD.Value<PluginLayoutControlsDisplay>('outside', { isHidden: true })
  31. };
  32. export type PluginLayoutStateProps = PD.Values<typeof PluginLayoutStateParams>
  33. export type LeftPanelTabName = 'none' | 'root' | 'data' | 'states' | 'settings' | 'help'
  34. interface RootState {
  35. top: string | null,
  36. bottom: string | null,
  37. left: string | null,
  38. right: string | null,
  39. width: string | null,
  40. height: string | null,
  41. maxWidth: string | null,
  42. maxHeight: string | null,
  43. margin: string | null,
  44. marginLeft: string | null,
  45. marginRight: string | null,
  46. marginTop: string | null,
  47. marginBottom: string | null,
  48. scrollTop: number,
  49. scrollLeft: number,
  50. position: string | null,
  51. overflow: string | null,
  52. viewports: HTMLElement[],
  53. zIndex: string | null
  54. }
  55. export class PluginLayout extends StatefulPluginComponent<PluginLayoutStateProps> {
  56. readonly events = {
  57. updated: this.ev()
  58. }
  59. private updateProps(state: Partial<PluginLayoutStateProps>) {
  60. const prevExpanded = !!this.state.isExpanded;
  61. this.updateState(state);
  62. if (this.root && typeof state.isExpanded === 'boolean' && state.isExpanded !== prevExpanded) this.handleExpand();
  63. this.events.updated.next();
  64. }
  65. root: HTMLElement | undefined;
  66. private rootState: RootState | undefined = void 0;
  67. private expandedViewport: HTMLMetaElement;
  68. setProps(props: Partial<PluginLayoutStateProps>) {
  69. this.updateState(props);
  70. }
  71. setRoot(root: HTMLElement) {
  72. this.root = root;
  73. if (this.state.isExpanded) this.handleExpand();
  74. }
  75. private getScrollElement() {
  76. if ((document as any).scrollingElement) return (document as any).scrollingElement;
  77. if (document.documentElement) return document.documentElement;
  78. return document.body;
  79. }
  80. private handleExpand() {
  81. try {
  82. const body = document.getElementsByTagName('body')[0];
  83. const head = document.getElementsByTagName('head')[0];
  84. if (!body || !head || !this.root) return;
  85. if (this.state.isExpanded) {
  86. const children = head.children;
  87. const viewports: HTMLElement[] = [];
  88. let hasExp = false;
  89. for (let i = 0; i < children.length; i++) {
  90. if (children[i] === this.expandedViewport) {
  91. hasExp = true;
  92. } else if (((children[i] as any).name || '').toLowerCase() === 'viewport') {
  93. viewports.push(children[i] as any);
  94. }
  95. }
  96. for (let v of viewports) {
  97. head.removeChild(v);
  98. }
  99. if (!hasExp) head.appendChild(this.expandedViewport);
  100. const s = body.style;
  101. const doc = this.getScrollElement();
  102. const scrollLeft = doc.scrollLeft;
  103. const scrollTop = doc.scrollTop;
  104. this.rootState = {
  105. top: s.top, bottom: s.bottom, right: s.right, left: s.left, scrollTop, scrollLeft, position: s.position, overflow: s.overflow, viewports, zIndex: this.root.style.zIndex,
  106. width: s.width, height: s.height,
  107. maxWidth: s.maxWidth, maxHeight: s.maxHeight,
  108. margin: s.margin, marginLeft: s.marginLeft, marginRight: s.marginRight, marginTop: s.marginTop, marginBottom: s.marginBottom
  109. };
  110. s.overflow = 'hidden';
  111. s.position = 'fixed';
  112. s.top = '0';
  113. s.bottom = '0';
  114. s.right = '0';
  115. s.left = '0';
  116. s.width = '100%';
  117. s.height = '100%';
  118. s.maxWidth = '100%';
  119. s.maxHeight = '100%';
  120. s.margin = '0';
  121. s.marginLeft = '0';
  122. s.marginRight = '0';
  123. s.marginTop = '0';
  124. s.marginBottom = '0';
  125. // TODO: setting this breaks viewport controls for some reason. Is there a fix?
  126. // this.root.style.zIndex = '100000';
  127. } else {
  128. const children = head.children;
  129. for (let i = 0; i < children.length; i++) {
  130. if (children[i] === this.expandedViewport) {
  131. head.removeChild(this.expandedViewport);
  132. break;
  133. }
  134. }
  135. if (this.rootState) {
  136. const t = this.rootState;
  137. for (let v of t.viewports) {
  138. head.appendChild(v);
  139. }
  140. const s = body.style;
  141. s.top = t.top!;
  142. s.bottom = t.bottom!;
  143. s.left = t.left!;
  144. s.right = t.right!;
  145. s.width = t.width!;
  146. s.height = t.height!;
  147. s.maxWidth = t.maxWidth!;
  148. s.maxHeight = t.maxHeight!;
  149. s.margin = t.margin!;
  150. s.marginLeft = t.marginLeft!;
  151. s.marginRight = t.marginRight!;
  152. s.marginTop = t.marginTop!;
  153. s.marginBottom = t.marginBottom!;
  154. s.position = t.position!;
  155. s.overflow = t.overflow || '';
  156. const doc = this.getScrollElement();
  157. doc.scrollTop = t.scrollTop;
  158. doc.scrollLeft = t.scrollLeft;
  159. this.rootState = void 0;
  160. this.root.style.zIndex = t.zIndex!;
  161. }
  162. }
  163. } catch (e) {
  164. const msg = 'Layout change error, you might have to reload the page.';
  165. this.context.log.error(msg);
  166. console.error(msg, e);
  167. }
  168. }
  169. constructor(private context: PluginContext) {
  170. super({ ...PD.getDefaultValues(PluginLayoutStateParams), ...(context.spec.layout && context.spec.layout.initial) });
  171. PluginCommands.Layout.Update.subscribe(context, e => this.updateProps(e.state));
  172. // TODO how best make sure it runs on node.js as well as in the browser?
  173. if (typeof document !== 'undefined') {
  174. // <meta name='viewport' content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0' />
  175. this.expandedViewport = document.createElement('meta') as any;
  176. this.expandedViewport.name = 'viewport';
  177. this.expandedViewport.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0';
  178. }
  179. }
  180. }