lodash#clamp TypeScript Examples

The following examples show how to use lodash#clamp. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: data-cell.ts    From S2 with MIT License 6 votes vote down vote up
/**
   * 计算柱图的 scale 函数(两种情况)
   *
   * min_________x_____0___________________________max
   * |<----r---->|
   *
   * 0_________________min_________x_______________max
   * |<-------------r------------->|
   *
   * @param minValue in current field values
   * @param max in current field values
   */
  private getIntervalScale(minValue = 0, maxValue = 0) {
    minValue = parseNumberWithPrecision(minValue);
    maxValue = parseNumberWithPrecision(maxValue);

    const realMin = minValue >= 0 ? 0 : minValue;
    const distance = maxValue - realMin || 1;
    return (current: number) =>
      // max percentage shouldn't be greater than 100%
      // min percentage shouldn't be less than 0%
      clamp((current - realMin) / distance, 0, 1);
  }
Example #2
Source File: EventStreamGraph.tsx    From next-basics with GNU General Public License v3.0 6 votes vote down vote up
private transform(dx: number, dy: number): void {
    const nodeWidth = styleConfig.node.width;
    const maxOffsetX = this.nodesContainerWidth - nodeWidth / 2;
    const maxOffsetY = this.nodesContainerHeight - nodeWidth / 2;
    const minOffsetX = -this.canvas.node().offsetWidth + nodeWidth / 2;
    const minOffsetY = -this.canvas.node().offsetHeight + nodeWidth / 2;
    const resultX = this.offsetX + dx;
    const resultY = this.offsetY + dy;
    this.offsetX = clamp(resultX, minOffsetX, maxOffsetX);
    this.offsetY = clamp(resultY, minOffsetY, maxOffsetY);
    const transformToNodes = `translate(${-this.offsetX}px, ${-this
      .offsetY}px)`;
    const transform = zoomIdentity.translate(-this.offsetX, -this.offsetY);
    this.linksLayer.attr("transform", transform.toString());
    this.nodesLayer.style("transform", transformToNodes);
  }
Example #3
Source File: Accelerometer.ts    From disco-cube-daemon with MIT License 6 votes vote down vote up
constructor() {
    const port = new SerialPort("/dev/ttyACM0", {
      baudRate: 115200,
    });
    const parser = new Readline();

    port.pipe(parser);
    parser.on("data", (data: string) => {
      const parts = data.trim().split(",").map(s => clamp(Number.parseInt(s) / 1000, -1, 1)).map(n => Number.isNaN(n) ? 0 : n)
      if (parts.length != 3) return;
      this.accel = parts;
    });

  }
Example #4
Source File: getNumThreads.ts    From nextclade with MIT License 6 votes vote down vote up
export function guessNumThreads() {
  const memoryBytesAvailable = getMemoryBytesAvailable()
  if (memoryBytesAvailable && Number.isFinite(memoryBytesAvailable)) {
    const numThreadsMax = Math.floor(memoryBytesAvailable / MEMORY_BYTES_PER_THREAD_MINIMUM)
    const numThreads = clamp(numThreadsMax, MINIMUM_NUM_THREADS, MAXIMUM_NUM_THREADS)
    return { memoryAvailable: memoryBytesAvailable, numThreads }
  }
  return undefined
}
Example #5
Source File: BitcoinNode.ts    From sapio-studio with Mozilla Public License 2.0 6 votes vote down vote up
async periodic_check() {
        const contract = this.props.current_contract;
        console.info('PERIODIC CONTRACT CHECK');
        if (!contract.should_update()) {
            // poll here faster
            this.next_periodic_check = setTimeout(
                this.periodic_check.bind(this),
                1000
            );
            return;
        }
        const status = await this.get_transaction_status(contract);
        const state = compute_impossible(
            derive_state(status, this.props.current_contract),
            this.props.current_contract
        );
        store.dispatch(load_status({ status, state }));

        if (this.mounted) {
            const freq = selectNodePollFreq(store.getState());
            const period = clamp(freq ?? 0, 5, 60 * 5);

            console.info('NEXT PERIODIC CONTRACT CHECK ', period, ' SECONDS');
            this.next_periodic_check = setTimeout(
                this.periodic_check.bind(this),
                1000 * period
            );
        }
    }
Example #6
Source File: rasa-chatbot.tsx    From covid-19 with MIT License 5 votes vote down vote up
customMessageDelay = (message: string) => {
  if (message === 'undefined') return 0

  const delay = message.length * 5
  return clamp(delay, 500, 1500)
}
Example #7
Source File: propertyMerge.ts    From next-core with GNU General Public License v3.0 5 votes vote down vote up
function propertyMergeAllOfArray(
  { baseValue, context, proxies }: MergeBase,
  object: Record<string, unknown>
): unknown[] {
  // Use an approach like template-literal's quasis:
  // `quasi0${0}quais1${1}quasi2...`
  // Every quasi can be merged with multiple items.
  const computedBaseValue = Array.isArray(baseValue)
    ? (computeRealValue(baseValue, context, true, {
        $$lazyForUseBrick: true,
      }) as unknown[])
    : [];
  const quasis: unknown[][] = [];
  const size = computedBaseValue.length + 1;
  for (let i = 0; i < size; i += 1) {
    quasis.push([]);
  }

  for (const proxy of proxies as MergeablePropertyProxyOfArray[]) {
    let position: number;
    switch (proxy.mergeMethod) {
      case "append":
        position = computedBaseValue.length;
        break;
      case "prepend":
        position = 0;
        break;
      case "insertAt":
        // Defaults to `-1`.
        position = (proxy.mergeArgs as [number])?.[0] ?? -1;
        if (position < 0) {
          // It's counted from the end if position is negative.
          position += quasis.length;
        }
        position = clamp(position, 0, computedBaseValue.length);
        break;
      // istanbul ignore next: should never reach
      default:
        throw new TypeError(
          `unsupported mergeMethod: "${proxy.mergeMethod}" for mergeType "${proxy.mergeType}"`
        );
    }
    let patchValue = object[proxy.$$reversedRef] as unknown[];
    if (!Array.isArray(patchValue)) {
      patchValue = [];
    }
    quasis[position].push(...patchValue);
  }

  return quasis.flatMap((item, index) =>
    index < computedBaseValue.length
      ? item.concat(computedBaseValue[index])
      : item
  );
}
Example #8
Source File: roam-vim-panel.ts    From roam-toolkit with MIT License 5 votes vote down vote up
private static at(panelIndex: PanelIndex): VimRoamPanel {
        panelIndex = clamp(panelIndex, 0, state.panelOrder.length - 1)
        return VimRoamPanel.get(state.panelOrder[panelIndex])
    }
Example #9
Source File: BitcoinStatusBar.tsx    From sapio-studio with Mozilla Public License 2.0 5 votes vote down vote up
export function BitcoinStatusBar(props: BitcoinStatusBarProps) {
    const theme = useTheme();
    const freq = useSelector(selectNodePollFreq);
    const [balance, setBalance] = React.useState<number>(0);
    const [blockchaininfo, setBlockchaininfo] = React.useState<any>(null);
    React.useEffect(() => {
        let next: ReturnType<typeof setTimeout> | null = null;
        let mounted = true;
        const periodic_update_stats = async () => {
            next = null;
            try {
                const balance = await props.api.check_balance();
                setBalance(balance);
            } catch (err) {
                console.error(err);
                setBalance(0);
            }

            try {
                const info = await props.api.blockchaininfo();
                console.log(balance);
                setBlockchaininfo(info);
            } catch (err) {
                console.error(err);
                setBlockchaininfo(null);
            }

            if (mounted) {
                let prefs = freq;
                prefs = clamp(prefs ?? 0, 5, 5 * 60);
                console.log('StatusBar', 'NEXT PERIODIC CHECK IN ', prefs);
                next = setTimeout(periodic_update_stats, prefs * 1000);
            }
        };

        let prefs = freq;
        prefs = clamp(prefs ?? 0, 5, 5 * 60);
        next = setTimeout(periodic_update_stats, prefs * 1000);
        return () => {
            mounted = false;
            if (next !== null) clearTimeout(next);
        };
    }, []);

    const network = blockchaininfo?.chain ?? 'disconnected';
    const headers = blockchaininfo?.headers ?? '?';
    const blocks = blockchaininfo?.headers ?? '?';
    return (
        <Paper
            square={true}
            sx={{
                top: 'auto',
                bottom: 0,
            }}
            className="BitcoinStatusBar Draggable"
            style={{
                background: theme.palette.background.default,
                color: theme.palette.info.main,
            }}
        >
            <Toolbar variant="dense">
                <Typography variant="h6" color="inherit" component="div">
                    <div>chain: {network}</div>
                </Typography>
                <Typography variant="h6" color="inherit" component="div">
                    <div style={{ marginLeft: '0.5em' }}>
                        balance: {balance} BTC
                    </div>
                </Typography>
                <Typography variant="h6" color="inherit" component="div">
                    <div style={{ marginLeft: '0.5em' }}>
                        processed: {blocks}/{headers}
                    </div>
                </Typography>
            </Toolbar>
        </Paper>
    );
}
Example #10
Source File: SearchWizardForm.tsx    From one-platform with MIT License 4 votes vote down vote up
export default function SearchWizardForm(props: any) {
  const { appId, loading: appLoading } = useContext(AppContext);
  const { searchConfig, loading: searchConfigLoading } = useSearchConfig(appId);
  const [currentStepId, setCurrentStepId] = useState<number>(0);
  const { actions, state } = useStateMachine({ saveState, overrideState });

  const [defaultValues, setDefaultValues] = useState<App.SearchConfig>();

  useEffect( () => {
    setCurrentStepId( state.savedStepId ?? 0 );
  }, [state.savedStepId] );

  useEffect(() => {
    if (!isEmpty(searchConfig)) {
      setDefaultValues( searchConfig );
      if ( isEmpty( state.formData ) || state.appId !== appId ) {
        actions.saveState( { appId, formData: { ...searchConfig } } );
      }
    }
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [actions, appId, searchConfig] );

  const onMove = useCallback(( stepId: number ) => {
    setCurrentStepId( stepId );
    actions.saveState({
      savedStepId: ( !state.savedStepId || state.savedStepId < stepId )
        ? stepId
        : state.savedStepId,
    });
  }, [actions, state.savedStepId]);

  const onNext = useCallback( () => {
    onMove( clamp( currentStepId + 1, 0, 3 ) );
  }, [currentStepId, onMove] );

  const onBack = useCallback( () => {
    onMove( clamp( currentStepId - 1, 0, 3 ) );
  }, [currentStepId, onMove])

  const saveSearchConfig = useCallback( () => {
    const { formData } = state;
    updateSearchConfigService( appId, formData )
      .then( res => {
        window.OpNotification?.success({
          subject: 'Search Configuration Saved Successfully!',
        } );
        actions.saveState( { savedStepId: 0 } );
      } )
      .catch( err => {
        window.OpNotification?.danger({
          subject: 'An error occurred when saving the Search Config.',
          body: 'Please try again later.',
        });
        console.error(err);
      } );
  }, [actions, appId, state] );

  const resetForm = useCallback(() => {
    actions.overrideState( {
      formData: defaultValues,
      savedStepId: 0,
    } );
  }, [actions, defaultValues] );

  const stepTabs = useMemo( () => {
    return steps.map( ( step, index ) => {
      const isStepActive = currentStepId === index;
      const stepStyles: CSSProperties = {
        marginRight: '0.5rem',
        borderWidth: '1px',
        borderStyle: 'solid',
        borderRadius: '2rem',
        width: '1.5rem',
        lineHeight: '1.5rem',
        display: 'inline-grid',
        placeContent: 'center',
      };
      if ( isStepActive ) {
        stepStyles.color = '#fff';
        stepStyles.borderColor = stepStyles.backgroundColor =
          'var(--pf-global--primary-color--100)';
      }

      return (
        <FlexItem key={index} flex={{ default: 'flex_1' }}>
          <Button isBlock variant="plain" isDisabled={state.savedStepId < index}>
            <Text
              className={ isStepActive
                ? 'pf-u-primary-color-100 pf-u-font-weight-bold'
                : ''
              }
            >
              <span style={{ ...stepStyles }}>{index + 1}</span>
              {step.label}
            </Text>
          </Button>
        </FlexItem>
      );});
  }, [currentStepId, state.savedStepId] );

  const currentStep = useMemo(() => {
    const Step = steps[currentStepId].component;

    const props: IConfigureSearchStepProps = {
      onNext,
      onBack,
      onReset: resetForm,
    };
    if (currentStepId === 0) {
      props.onBack = undefined;
    }
    if (currentStepId === steps.length - 1) {
      props.onNext = saveSearchConfig;
      props.nextButtonText = 'Save'
    }
    return <Step {...props} />;
  }, [currentStepId, onBack, onNext, resetForm, saveSearchConfig] );

  return (
    <>
      <Stack hasGutter>
        <StackItem>
          <Header title="Configure Search" />
        </StackItem>

        {(appLoading || searchConfigLoading) && <Loader />}

        {!appLoading && !searchConfigLoading && (
          <>
            <StackItem>
              <Flex justifyContent={{ default: 'justifyContentSpaceAround' }}>
                {stepTabs}
              </Flex>
            </StackItem>
            <StackItem>
              <Card isRounded>
                <CardBody>{currentStep}</CardBody>
              </Card>
            </StackItem>
          </>
        )}
      </Stack>
    </>
  );
}
Example #11
Source File: ClipboardRenderer.tsx    From DittoPlusPlus with MIT License 4 votes vote down vote up
ClipboardRenderer = (props: PluginTypes.RenderProps) => {
  const [clipItems, updateClipItems] = useState<ClipItemDoc[]>([]);
  const [selectedIndex, updateSelectedIndex] = useState(0);
  const [imagesDir, updateImagesDir] = useState<string>('');
  const [searchText, updateSearchText] = useState('');
  const {pluginProcess} = props;
  const searchBarRef = useRef<HTMLInputElement>(null);
  const clipsListRef = useRef<HTMLDivElement>(null);

  const resetClips = () => {
    pluginProcess.send(Messages.GetAllClipItems, undefined, (err, clips) => {
      if (!err) {
        updateClipItems([...clips]);
      }
    });
  };

  const hideWindow = () => {
    setImmediate(() => {
      ipcRenderer.send(GlobalEvents.HideWindow);
    });
  };

  const onKeyPress = (event: KeyboardEvent) => {
    const {keyCode} = event;

    /* disable scrolling by arrow keys */
    if ([38, 40].includes(keyCode)) {
      event.preventDefault();
    }

    if (clipsListRef.current) {
      const {clipItemDimensions, searchBarDimensions} = dimensions;

      const clipRowHeight =
        clipItemDimensions.heightPx +
        clipItemDimensions.paddingTopPx +
        clipItemDimensions.paddingBottomPx;

      const searchBarHeight =
        searchBarDimensions.heightPx +
        searchBarDimensions.paddingTopPx +
        searchBarDimensions.paddingBottomPx;

      const viewHeight = clipsListRef.current.offsetHeight - searchBarHeight;
      const itemsVisibleN = Math.floor(viewHeight / clipRowHeight);

      const itemsScrolled = Math.floor(
        clipsListRef.current.scrollTop / clipRowHeight,
      );

      const isItemInViewPort = inRange(
        selectedIndex,
        itemsScrolled,
        itemsVisibleN + itemsScrolled + 1,
      );

      /* up key */
      if (keyCode === 38) {
        if (isItemInViewPort) {
          clipsListRef.current.scrollBy({
            top: -clipRowHeight,
          });
        } else {
          clipsListRef.current.scrollTop = (selectedIndex - 2) * clipRowHeight;
        }

        updateSelectedIndex((prevSelectedIndex) =>
          clamp(prevSelectedIndex - 1, 0, clipItems.length - 1),
        );
      }

      /* down key */
      if (keyCode === 40) {
        if (selectedIndex >= itemsVisibleN - 1 && isItemInViewPort) {
          clipsListRef.current.scrollBy({top: clipRowHeight});
        } else if (clipsListRef.current.scrollTop) {
          clipsListRef.current.scrollTop = selectedIndex * clipRowHeight;
        }

        updateSelectedIndex((prevSelectedIndex) =>
          clamp(prevSelectedIndex + 1, 0, clipItems.length - 1),
        );
      }
    }

    /* escape */
    if (keyCode === 27) {
      if (searchText) {
        resetClips();
      } else {
        hideWindow();
      }

      handleSearchUpdate('');
    }

    /* enter key */
    if (keyCode === 13) {
      handleClipItemSelected(clipItems[selectedIndex]);
    }

    /* key is alphanumeric */
    if (isAlphanumeric(keyCode)) {
      updateSelectedIndex(0);
      searchBarRef.current && searchBarRef.current.focus();
    }
  };

  useEventListener('keydown', onKeyPress);

  useEffect(() => {
    resetClips();

    pluginProcess.on(Events.NewClip, (clip: ClipItemDoc) => {
      updateClipItems((prevClipItems) => [clip, ...prevClipItems]);
    });

    pluginProcess.on(Events.ClipsInitialized, (clips: ClipItemDoc[]) => {
      updateClipItems([...clips]);
    });

    ipcRenderer.on(GlobalEvents.ShowWindow, () => {
      if (process.platform === 'linux') {
        setImmediate(() => {
          searchBarRef.current && searchBarRef.current.blur();
        });
      }
    });

    ipcRenderer.on(GlobalEvents.HideWindow, () => {
      handleSearchUpdate('');
    });

    pluginProcess.send(
      Messages.GetImagesDir,
      undefined,
      (_err: any, _imagesDir: string) => {
        updateImagesDir(_imagesDir);
      },
    );
  }, []);

  const handleClipItemSelected = (item: ClipItemDoc) => {
    pluginProcess.send(Messages.ClipItemSelected, item, (err, res) => {
      if (err) {
        throw err;
      }

      if (res) {
        updateClipItems([...res]);
      }
    });

    handleSearchUpdate('');
    hideWindow();
  };

  const onClickClipItem = (item: ClipItemDoc) => {
    handleClipItemSelected(item);
  };

  const handleSearchUpdate = (text: string) => {
    updateSearchText(text);
    updateSelectedIndex(0);
    clipsListRef.current && (clipsListRef.current.scrollTop = 0);

    if (text === '') {
      searchBarRef.current && searchBarRef.current.blur();
      resetClips();
    } else {
      pluginProcess.send(Messages.SearchClips, text, (err, clips) => {
        if (err) {
          throw err;
        }

        updateClipItems([...clips]);
      });
    }
  };

  const onSearchTextChanged = (
    event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>,
  ) => {
    const query = event.target.value;
    handleSearchUpdate(query);
  };

  const getClipItemVariant = (index: number): ClipItemVariants => {
    if (index === selectedIndex) {
      return 'selected';
    }

    if (index % 2 === 0) {
      return 'dark';
    } else {
      return 'light';
    }
  };

  return (
    <StyledContainer>
      <SimpleBar
        style={SimpleBarStyles}
        scrollableNodeProps={{ref: clipsListRef}}
      >
        {clipItems.map((item, index) => (
          <ClipItem
            key={`${index}_clipItem`}
            clipItem={item}
            imagesDir={imagesDir}
            searchText={searchText}
            variant={getClipItemVariant(index)}
            onClick={() => onClickClipItem(item)}
          />
        ))}
      </SimpleBar>
      <SearchBar
        id="clipboard-searchbar"
        placeholder="search"
        onChange={onSearchTextChanged}
        value={searchText}
        ref={searchBarRef}
      />
    </StyledContainer>
  );
}
Example #12
Source File: EditWaypointForm.tsx    From project-tauntaun with GNU Lesser General Public License v3.0 4 votes vote down vote up
export function EditWaypointForm(props: EditWaypointFormProps) {
  const { selectWaypoint } = SelectionStateContainer.useContainer();

  const { group, pointIndex } = props;
  const point = group.points[pointIndex];
  const movingPoint = point as MovingPoint;

  const [alt, setAlt] = useState(point.alt);
  const [altType, setAltType] = useState(movingPoint ? movingPoint.alt_type : 0);
  const [type, setType] = useState(point.type);
  const [name, setName] = useState(point.name);
  const [speed, setSpeed] = useState(point.speed);
  const [action, setAction] = useState(point.action);
  const [currentPointIndex, setCurrentPointIndex] = useState(pointIndex);
  const [useImperial, setImperial] = useState(true);

  if (pointIndex !== currentPointIndex) {
    setAlt(point.alt);
    setType(point.type);
    setName(point.name);
    setSpeed(point.speed);
    setAction(point.action);
    setCurrentPointIndex(pointIndex);

    if (movingPoint) {
      setAltType(movingPoint.alt_type);
    }
  }

  const actionsOptions = Object.keys(PointAction).map((key, value) => ({
    value: key,
    label: Object.values(PointAction)[value]
  }));

  const saveWaypointOnClick = () => {
    // TODO validate
    gameService.sendRouteModify(group, point.position, {
      ...point,
      alt: alt,
      alt_type: altType,
      type: type,
      name: name,
      speed: speed,
      action: action
    } as MovingPoint);

    selectWaypoint(undefined);
  };

  const onActionChange = (event: any) => {
    setType(PointAction[event.target.value]);
    setAction(event.target.value);
  };

  const closeOnClick = () => selectWaypoint(undefined);

  const onUnitsSystemChange = () => {
    setImperial(!useImperial);
  };

  const onAltTypeChange = (event: React.ChangeEvent<HTMLInputElement>) =>
    setAltType(event.target.checked ? AltType.BARO : AltType.RADIO);

  const onWpNumberUp = () => {
    selectWaypoint(clamp(currentPointIndex + 1, 0, group.points.length - 1));
  };

  const onWpNumberDown = () => {
    selectWaypoint(clamp(currentPointIndex - 1, 0, group.points.length - 1));
  };

  const onSetTargetClicked = () => {
    setAltType(AltType.RADIO);
    setAlt(0);
  };

  const GraySwitch = withStyles({
    switchBase: {
      color: 'rgba(49, 107, 170, 1)',
      $track: {
        backgroundColor: 'rgba(0,0,0,0.38)'
      }
    },
    checked: {},
    track: {}
  })(Switch);

  return (
    <section className="EditWaypointContainer">
      <header>
        <h2>Group name: {group.name}</h2>
      </header>
      <div>
        <div className="WaypointNumber">
          <div>
            <span>Waypoint</span>
            <p>{currentPointIndex}</p>
          </div>
          <div className="WaypointButtonContainer">
            <button onClick={onWpNumberDown}>Prev</button>
            <button onClick={onWpNumberUp}>Next</button>
          </div>
        </div>
      </div>
      <div className="WaypointAltitude">
        <span className="WaypointAltitudeTitle">Altitude</span>
        <input
          type="text"
          pattern="[0-100000]"
          value={useImperial ? alt * c_MeterToFeet : alt}
          onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
            setAlt(useImperial ? +event.target.value / c_MeterToFeet : +event.target.value);
          }}
        />
        <div className="Toggle">
          <span>Meters</span>
          <FormControlLabel
            control={
              <GraySwitch
                checked={useImperial}
                onChange={onUnitsSystemChange}
                name="Use Imperial"
                size="small"
                color="default"
              />
            }
            label=""
          />
          <span>Feet</span>
        </div>
        {altType && (
          <div className="Toggle">
            <span>AGL</span>
            <FormControlLabel
              control={
                <GraySwitch
                  checked={altType === AltType.BARO}
                  onChange={onAltTypeChange}
                  name="Use Imperial"
                  size="small"
                  color="default"
                />
              }
              label=""
            />
            <span>MSL</span>
          </div>
        )}
      </div>
      <button onClick={onSetTargetClicked}>Set as ground target</button>
      <div className="fieldContainer">
        <span>Name</span>
        <input
          type="text"
          value={name}
          onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
            setName(event.target.value);
          }}
        />
      </div>
      <div className="fieldContainer">
        <span>Speed</span>
        <input
          type="text"
          pattern="[0-2]{0,1}[0-9]{1,3}[\.,][0-9]+"
          value={speed}
          onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
            setSpeed(+event.target.value);
          }}
        />
      </div>
      <div className="fieldContainer">
        <span>Action</span>
        <Select onChange={onActionChange} value={action}>
          {actionsOptions.map((option, i) => (
            <MenuItem key={`actionsOptions${i}`} value={option.value}>
              {option.label}
            </MenuItem>
          ))}
        </Select>
      </div>
      <div>
        <button onClick={saveWaypointOnClick}>Save waypoint</button>
        <button onClick={closeOnClick}>Close</button>
      </div>
    </section>
  );
}
Example #13
Source File: setupTemplateProxy.ts    From next-core with GNU General Public License v3.0 4 votes vote down vote up
export function setupTemplateProxy(
  proxyContext: Partial<ProxyContext>,
  ref: string,
  slots: SlotsConfOfBricks
): RuntimeBrickConfOfTplSymbols {
  const computedPropsFromProxy: Record<string, unknown> = {};
  let refForProxy: RefForProxy;

  const {
    reversedProxies,
    templateProperties,
    externalSlots,
    templateContextId,
    proxyBrick,
  } = proxyContext;

  if (ref && reversedProxies) {
    refForProxy = {};
    proxyBrick.proxyRefs.set(ref, refForProxy);

    // Reversed proxies are used for expand storyboard before rendering page.
    if (reversedProxies.properties.has(ref)) {
      Object.assign(
        computedPropsFromProxy,
        Object.fromEntries(
          reversedProxies.properties
            .get(ref)
            .flatMap((propRef) => {
              // `propValue` is computed.
              const propValue = templateProperties?.[propRef.$$reversedRef];
              if (isTransformableProperty(propRef)) {
                return Object.entries(
                  preprocessTransformProperties(
                    {
                      [propRef.$$reversedRef]: propValue,
                    },
                    propRef.refTransform
                  )
                );
              }
              if (isBasicProperty(propRef)) {
                return [[propRef.refProperty, propValue]];
              }
              // Ignore Variable properties.
              // And mergeable properties are processed later.
              return [];
            })
            .filter((propRef) => propRef[1] !== undefined)
        )
      );

      // Brick properties can be merged multiple times.
      if (reversedProxies.mergeBases.has(ref)) {
        Object.assign(
          computedPropsFromProxy,
          Object.fromEntries(
            Array.from(reversedProxies.mergeBases.get(ref).entries())
              .map(([mergeProperty, mergeBase]) => [
                mergeProperty,
                propertyMergeAll(mergeBase, templateProperties ?? {}),
              ])
              .filter((item) => item[1] !== undefined)
          )
        );
      }
    }

    // Use an approach like template-literal's quasis:
    // `quasi0${0}quais1${1}quasi2...`
    // Every quasi (indexed by `refPosition`) can be slotted with multiple bricks.
    const quasisMap = new Map<string, BrickConf[][]>();

    if (reversedProxies.slots.has(ref)) {
      for (const item of reversedProxies.slots.get(ref)) {
        if (!quasisMap.has(item.refSlot)) {
          const quasis: BrickConf[][] = [];
          // The size of quasis should be the existed slotted bricks' size plus one.
          const size = hasOwnProperty(slots, item.refSlot)
            ? slots[item.refSlot].bricks.length + 1
            : 1;
          for (let i = 0; i < size; i += 1) {
            quasis.push([]);
          }
          quasisMap.set(item.refSlot, quasis);
        }
        const expandableSlot = quasisMap.get(item.refSlot);
        const refPosition = item.refPosition ?? -1;
        expandableSlot[
          clamp(
            refPosition < 0 ? expandableSlot.length + refPosition : refPosition,
            0,
            expandableSlot.length - 1
          )
        ].push(...(externalSlots?.[item.$$reversedRef]?.bricks ?? []));
      }
    }

    for (const [slotName, quasis] of quasisMap.entries()) {
      if (!hasOwnProperty(slots, slotName)) {
        slots[slotName] = {
          type: "bricks",
          bricks: [],
        };
      }
      const slotConf = slots[slotName];
      slotConf.bricks = quasis.flatMap((bricks, index) =>
        index < slotConf.bricks.length
          ? bricks.concat(slotConf.bricks[index])
          : bricks
      );

      if (slotConf.bricks.length === 0) {
        delete slots[slotName];
      }
    }
  }

  return {
    [symbolForComputedPropsFromProxy]: computedPropsFromProxy,
    [symbolForRefForProxy]: refForProxy,
    [symbolForTplContextId]: templateContextId,
  };
}
Example #14
Source File: DropZone.tsx    From next-core with GNU General Public License v3.0 4 votes vote down vote up
export function DropZone({
  nodeUid,
  isRoot,
  separateCanvas,
  isPortalCanvas,
  independentPortalCanvas,
  canvasIndex,
  mountPoint,
  fullscreen,
  delegatedContext,
  dropZoneStyle,
  dropZoneBodyStyle,
  slotContentLayout,
  showOutlineIfEmpty,
  hiddenWrapper = true,
  emptyClassName,
}: DropZoneProps): React.ReactElement {
  const dropZoneBody = React.useRef<HTMLDivElement>();
  const [dropPositionCursor, setDropPositionCursor] =
    React.useState<DropPositionCursor>(null);
  const dropPositionCursorRef = React.useRef<DropPositionCursor>();
  const contextMenuStatus = useBuilderContextMenuStatus();
  const manager = useBuilderDataManager();
  const { nodes, edges, wrapperNode } = useBuilderData();
  const isWrapper = hiddenWrapper && isRoot && !isEmpty(wrapperNode);
  const node = useBuilderNode({ nodeUid, isRoot, isWrapper });
  const groupedChildNodes = useBuilderGroupedChildNodes({
    nodeUid,
    isRoot,
    doNotExpandTemplates: isWrapper,
    isWrapper,
  });

  const isGeneralizedPortalCanvas = independentPortalCanvas
    ? canvasIndex > 0
    : isPortalCanvas;
  const hasTabs = separateCanvas || independentPortalCanvas;

  const canDrop = useCanDrop();
  const refinedSlotContentLayout =
    slotContentLayout ?? EditorSlotContentLayout.BLOCK;

  const selfChildNodes = React.useMemo(
    () =>
      groupedChildNodes.find((group) => group.mountPoint === mountPoint)
        ?.childNodes ?? [],
    [groupedChildNodes, mountPoint]
  );

  const canvasList = useCanvasList(selfChildNodes);

  const selfChildNodesInCurrentCanvas = React.useMemo(
    () =>
      separateCanvas
        ? selfChildNodes.filter((child) =>
            Boolean(Number(Boolean(isPortalCanvas)) ^ Number(!child.portal))
          )
        : independentPortalCanvas
        ? canvasList[clamp(canvasIndex ?? 0, 0, canvasList.length - 1)]
        : selfChildNodes,
    [
      canvasIndex,
      independentPortalCanvas,
      isPortalCanvas,
      selfChildNodes,
      canvasList,
      separateCanvas,
    ]
  );

  const canvasSettings = React.useMemo(
    () =>
      selfChildNodesInCurrentCanvas[0]?.$$parsedProperties
        ._canvas_ as BuilderCanvasSettings,
    [selfChildNodesInCurrentCanvas]
  );

  const getDroppingIndexInFullCanvas = React.useCallback(
    (droppingIndexInCurrentCanvas: number) => {
      if (!hasTabs) {
        return droppingIndexInCurrentCanvas;
      }
      if (selfChildNodesInCurrentCanvas.length > 0) {
        const cursorNode =
          selfChildNodesInCurrentCanvas[
            droppingIndexInCurrentCanvas === 0
              ? 0
              : droppingIndexInCurrentCanvas - 1
          ];
        return (
          selfChildNodes.findIndex((child) => child === cursorNode) +
          (droppingIndexInCurrentCanvas === 0 ? 0 : 1)
        );
      }
      return isGeneralizedPortalCanvas ? selfChildNodes.length : 0;
    },
    [
      hasTabs,
      selfChildNodesInCurrentCanvas,
      isGeneralizedPortalCanvas,
      selfChildNodes,
    ]
  );

  const getDroppingContext = React.useCallback(() => {
    if (delegatedContext) {
      const siblingGroups = getBuilderGroupedChildNodes({
        nodeUid: delegatedContext.templateUid,
        nodes,
        edges,
        doNotExpandTemplates: true,
      });
      return {
        droppingParentUid: delegatedContext.templateUid,
        droppingParentInstanceId: nodes.find(
          (item) => item.$$uid === delegatedContext.templateUid
        ).instanceId,
        droppingMountPoint: delegatedContext.templateMountPoint,
        droppingChildNodes:
          siblingGroups.find(
            (group) => group.mountPoint === delegatedContext.templateMountPoint
          )?.childNodes ?? [],
        droppingSiblingGroups: siblingGroups,
      };
    }
    return {
      droppingParentUid: node.$$uid,
      droppingParentInstanceId: isWrapper
        ? wrapperNode.instanceId
        : node.instanceId,
      droppingMountPoint: mountPoint,
      droppingChildNodes: selfChildNodes,
      droppingSiblingGroups: groupedChildNodes,
    };
  }, [
    delegatedContext,
    edges,
    groupedChildNodes,
    mountPoint,
    node,
    nodes,
    selfChildNodes,
    isWrapper,
    wrapperNode,
  ]);

  const [{ isDraggingOverCurrent }, dropRef] = useDrop({
    accept: [
      BuilderDataTransferType.NODE_TO_ADD,
      BuilderDataTransferType.NODE_TO_MOVE,
      BuilderDataTransferType.SNIPPET_TO_APPLY,
    ],
    canDrop: (
      item: DragObjectWithType &
        (
          | BuilderDataTransferPayloadOfNodeToAdd
          | BuilderDataTransferPayloadOfNodeToMove
        )
    ) =>
      independentPortalCanvas && isGeneralizedPortalCanvas
        ? selfChildNodesInCurrentCanvas.length === 0
        : item.type === BuilderDataTransferType.NODE_TO_ADD ||
          item.type === BuilderDataTransferType.SNIPPET_TO_APPLY ||
          isRoot ||
          canDrop(
            (item as BuilderDataTransferPayloadOfNodeToMove).nodeUid,
            nodeUid
          ),
    collect: (monitor) => ({
      isDraggingOverCurrent:
        monitor.isOver({ shallow: true }) && monitor.canDrop(),
    }),
    hover: (item, monitor) => {
      if (monitor.isOver({ shallow: true }) && monitor.canDrop()) {
        const { x, y } = monitor.getClientOffset();
        dropPositionCursorRef.current = getDropPosition(
          x,
          y,
          dropZoneBody.current.parentElement,
          dropZoneBody.current
        );
        setDropPositionCursor(dropPositionCursorRef.current);
      }
    },
    drop: (item, monitor) => {
      if (!monitor.didDrop()) {
        const { type, ...data } = item;
        processDrop({
          type: type as BuilderDataTransferType,
          data,
          droppingIndex: getDroppingIndexInFullCanvas(
            dropPositionCursorRef.current.index
          ),
          isPortalCanvas: isGeneralizedPortalCanvas,
          manager,
          ...getDroppingContext(),
        });
      }
    },
  });

  React.useEffect(() => {
    manager.updateDroppingStatus(
      delegatedContext ? delegatedContext.templateUid : node.$$uid,
      delegatedContext ? delegatedContext.templateMountPoint : mountPoint,
      isDraggingOverCurrent
    );
  }, [isDraggingOverCurrent, mountPoint, manager, delegatedContext, node]);

  const droppable =
    !!delegatedContext ||
    isWrapper ||
    !(node.$$isExpandableTemplate || node.$$isTemplateInternalNode);

  const dropZoneRef = React.useRef<HTMLElement>();

  const dropZoneRefCallback = React.useCallback(
    (element: HTMLElement) => {
      dropZoneRef.current = element;
      if (droppable) {
        dropRef(element);
      }
    },
    [dropRef, droppable]
  );

  const handleContextMenu = React.useCallback(
    (event: React.MouseEvent) => {
      // `event.stopPropagation()` not working across bricks.
      if (
        !isGeneralizedPortalCanvas &&
        isCurrentTargetByClassName(
          event.target as HTMLElement,
          dropZoneRef.current
        )
      ) {
        event.preventDefault();
        manager.contextMenuChange({
          active: true,
          node,
          x: event.clientX,
          y: event.clientY,
        });
      }
    },
    [isGeneralizedPortalCanvas, manager, node]
  );

  return (
    <div
      ref={dropZoneRefCallback}
      className={classNames(
        styles.dropZone,
        isRoot
          ? classNames(
              styles.isRoot,
              canvasSettings?.mode &&
                String(canvasSettings.mode)
                  .split(/\s+/g)
                  .map((mode) => styles[`mode-${mode}`]),
              {
                [styles.fullscreen]: fullscreen,
                [styles.hasTabs]: hasTabs,
              }
            )
          : styles.isSlot,
        {
          [styles.isPortalCanvas]: isGeneralizedPortalCanvas,
          [styles.dropping]: isDraggingOverCurrent,
          [styles.active]:
            isRoot &&
            contextMenuStatus.active &&
            contextMenuStatus.node.$$uid === node.$$uid,
          [styles.showOutlineIfEmpty]:
            !isRoot && showOutlineIfEmpty && selfChildNodes.length === 0,
          [styles.slotContentLayoutBlock]:
            refinedSlotContentLayout === EditorSlotContentLayout.BLOCK,
          [styles.slotContentLayoutInline]:
            refinedSlotContentLayout === EditorSlotContentLayout.INLINE,
          [styles.slotContentLayoutGrid]:
            refinedSlotContentLayout === EditorSlotContentLayout.GRID,
        }
      )}
      style={dropZoneStyle}
      onContextMenu={isRoot ? handleContextMenu : null}
    >
      <div
        ref={dropZoneBody}
        className={classNames(
          styles.dropZoneBody,
          selfChildNodesInCurrentCanvas.length === 0 && emptyClassName
        )}
        data-slot-id={mountPoint}
        style={dropZoneBodyStyle}
      >
        {selfChildNodesInCurrentCanvas.map((child) => (
          <EditorBrickAsComponent
            key={child.$$uid}
            node={child}
            slotContentLayout={refinedSlotContentLayout}
          />
        ))}
      </div>
      {
        <div
          className={classNames(
            styles.dropCursor,
            dropPositionCursor?.isVertical
              ? styles.dropCursorVertical
              : styles.dropCursorHorizontal
          )}
          style={{
            top: dropPositionCursor?.y,
            left: dropPositionCursor?.x,
            height: dropPositionCursor?.height,
          }}
        ></div>
      }
    </div>
  );
}
Example #15
Source File: expandTemplateEdges.ts    From next-core with GNU General Public License v3.0 4 votes vote down vote up
export function expandTemplateEdges({
  nodes,
  // Here expanded edges should be excluded.
  edges,
  rootId,
}: BuilderCanvasData): BuilderRuntimeEdge[] {
  const newEdges = edges.slice();
  const reorderedEdgesMap = new WeakMap<BuilderRuntimeEdge, number>();

  const walk = (
    uid: number,
    callback: (
      node: BuilderRuntimeNode,
      childEdges: BuilderRuntimeEdge[]
    ) => void
  ): void => {
    const node = nodes.find((item) => item.$$uid === uid);
    const childEdges = sortBy(
      edges.filter((edge) => edge.parent === node.$$uid),
      (edge) => edge.sort
    );
    callback(node, childEdges);
    for (const edge of childEdges) {
      walk(edge.child, callback);
    }
  };

  const expandEdgesThroughTemplateChain = (
    node: BuilderRuntimeNode,
    mountPoint: string,
    childUid: number
  ): void => {
    let proxySlotConf: CustomTemplateProxySlot;
    let delegateToParentUid: number;
    if (
      node.$$isExpandableTemplate &&
      node.$$templateProxy?.slots &&
      hasOwnProperty(node.$$templateProxy.slots, mountPoint) &&
      (proxySlotConf = node.$$templateProxy.slots[mountPoint]) &&
      (delegateToParentUid = node.$$templateRefToUid.get(proxySlotConf.ref))
    ) {
      const nextNode = nodes.find((n) => n.$$uid === delegateToParentUid);
      const nextMountPoint = proxySlotConf.refSlot;
      if (nextNode.$$isExpandableTemplate) {
        expandEdgesThroughTemplateChain(nextNode, nextMountPoint, childUid);
      } else {
        const expandedEdge: BuilderRuntimeEdge = {
          child: childUid,
          parent: delegateToParentUid,
          mountPoint: nextMountPoint,
          sort: undefined,
          $$isTemplateExpanded: true,
        };
        const siblingEdges = sortBy(
          newEdges.filter((edge) => edge.parent === delegateToParentUid),
          (edge) => reorderedEdgesMap.get(edge) ?? edge.sort
        );
        const internalEdges = siblingEdges.filter(
          (edge) => edge.$$isTemplateInternal
        );
        // For more details about refPosition implementation detail,
        // see `packages/brick-kit/src/core/CustomTemplates/expandCustomTemplate.ts`.
        const refPosition = proxySlotConf.refPosition ?? -1;
        const clampedRefPosition = clamp(
          refPosition < 0
            ? internalEdges.length + 1 + refPosition
            : refPosition,
          0,
          internalEdges.length
        );
        siblingEdges.splice(
          clampedRefPosition < internalEdges.length
            ? siblingEdges.findIndex(
                (edge) => edge === internalEdges[clampedRefPosition]
              )
            : siblingEdges.length,
          0,
          expandedEdge
        );
        siblingEdges.forEach((edge, index) => {
          reorderedEdgesMap.set(edge, index);
        });
        newEdges.push(expandedEdge);
      }
    }
  };

  walk(rootId, (node, childEdges) => {
    if (!node.$$isExpandableTemplate) {
      return;
    }
    for (const childEdge of childEdges) {
      // Recursively expand templates.
      expandEdgesThroughTemplateChain(
        node,
        childEdge.mountPoint,
        childEdge.child
      );
    }
  });

  if (newEdges.length > edges.length) {
    return newEdges.map((edge) => {
      const sort = reorderedEdgesMap.get(edge);
      return sort === undefined
        ? edge
        : {
            ...edge,
            sort,
          };
    });
  }

  return edges;
}