layout.ts 7.6 KB

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