|  | @@ -1,6 +1,7 @@
 | 
	
		
			
				|  |  |  import {
 | 
	
		
			
				|  |  |    ExcalidrawElement,
 | 
	
		
			
				|  |  |    ExcalidrawSelectionElement,
 | 
	
		
			
				|  |  | +  ExcalidrawTextElement,
 | 
	
		
			
				|  |  |    FontFamilyValues,
 | 
	
		
			
				|  |  |  } from "../element/types";
 | 
	
		
			
				|  |  |  import {
 | 
	
	
		
			
				|  | @@ -16,7 +17,7 @@ import {
 | 
	
		
			
				|  |  |    isInvisiblySmallElement,
 | 
	
		
			
				|  |  |    refreshTextDimensions,
 | 
	
		
			
				|  |  |  } from "../element";
 | 
	
		
			
				|  |  | -import { isLinearElementType } from "../element/typeChecks";
 | 
	
		
			
				|  |  | +import { isLinearElementType, isTextElement } from "../element/typeChecks";
 | 
	
		
			
				|  |  |  import { randomId } from "../random";
 | 
	
		
			
				|  |  |  import {
 | 
	
		
			
				|  |  |    DEFAULT_FONT_FAMILY,
 | 
	
	
		
			
				|  | @@ -235,6 +236,82 @@ const restoreElement = (
 | 
	
		
			
				|  |  |    }
 | 
	
		
			
				|  |  |  };
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +/**
 | 
	
		
			
				|  |  | + * Repairs contaienr element's boundElements array by removing duplicates and
 | 
	
		
			
				|  |  | + * fixing containerId of bound elements if not present. Also removes any
 | 
	
		
			
				|  |  | + * bound elements that do not exist in the elements array.
 | 
	
		
			
				|  |  | + *
 | 
	
		
			
				|  |  | + * NOTE mutates elements.
 | 
	
		
			
				|  |  | + */
 | 
	
		
			
				|  |  | +const repairContainerElement = (
 | 
	
		
			
				|  |  | +  container: Mutable<ExcalidrawElement>,
 | 
	
		
			
				|  |  | +  elementsMap: Map<string, Mutable<ExcalidrawElement>>,
 | 
	
		
			
				|  |  | +) => {
 | 
	
		
			
				|  |  | +  if (container.boundElements) {
 | 
	
		
			
				|  |  | +    // copy because we're not cloning on restore, and we don't want to mutate upstream
 | 
	
		
			
				|  |  | +    const boundElements = container.boundElements.slice();
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    // dedupe bindings & fix boundElement.containerId if not set already
 | 
	
		
			
				|  |  | +    const boundIds = new Set<ExcalidrawElement["id"]>();
 | 
	
		
			
				|  |  | +    container.boundElements = boundElements.reduce(
 | 
	
		
			
				|  |  | +      (
 | 
	
		
			
				|  |  | +        acc: Mutable<NonNullable<ExcalidrawElement["boundElements"]>>,
 | 
	
		
			
				|  |  | +        binding,
 | 
	
		
			
				|  |  | +      ) => {
 | 
	
		
			
				|  |  | +        const boundElement = elementsMap.get(binding.id);
 | 
	
		
			
				|  |  | +        if (boundElement && !boundIds.has(binding.id)) {
 | 
	
		
			
				|  |  | +          if (
 | 
	
		
			
				|  |  | +            isTextElement(boundElement) &&
 | 
	
		
			
				|  |  | +            // being slightly conservative here, preserving existing containerId
 | 
	
		
			
				|  |  | +            // if defined, lest boundElements is stale
 | 
	
		
			
				|  |  | +            !boundElement.containerId
 | 
	
		
			
				|  |  | +          ) {
 | 
	
		
			
				|  |  | +            (boundElement as Mutable<ExcalidrawTextElement>).containerId =
 | 
	
		
			
				|  |  | +              container.id;
 | 
	
		
			
				|  |  | +          }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +          acc.push(binding);
 | 
	
		
			
				|  |  | +          boundIds.add(binding.id);
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +        return acc;
 | 
	
		
			
				|  |  | +      },
 | 
	
		
			
				|  |  | +      [],
 | 
	
		
			
				|  |  | +    );
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +};
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/**
 | 
	
		
			
				|  |  | + * Repairs target bound element's container's boundElements array,
 | 
	
		
			
				|  |  | + * or removes contaienrId if container does not exist.
 | 
	
		
			
				|  |  | + *
 | 
	
		
			
				|  |  | + * NOTE mutates elements.
 | 
	
		
			
				|  |  | + */
 | 
	
		
			
				|  |  | +const repairBoundElement = (
 | 
	
		
			
				|  |  | +  boundElement: Mutable<ExcalidrawTextElement>,
 | 
	
		
			
				|  |  | +  elementsMap: Map<string, Mutable<ExcalidrawElement>>,
 | 
	
		
			
				|  |  | +) => {
 | 
	
		
			
				|  |  | +  const container = boundElement.containerId
 | 
	
		
			
				|  |  | +    ? elementsMap.get(boundElement.containerId)
 | 
	
		
			
				|  |  | +    : null;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  if (!container) {
 | 
	
		
			
				|  |  | +    boundElement.containerId = null;
 | 
	
		
			
				|  |  | +    return;
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  if (
 | 
	
		
			
				|  |  | +    container.boundElements &&
 | 
	
		
			
				|  |  | +    !container.boundElements.find((binding) => binding.id === boundElement.id)
 | 
	
		
			
				|  |  | +  ) {
 | 
	
		
			
				|  |  | +    // copy because we're not cloning on restore, and we don't want to mutate upstream
 | 
	
		
			
				|  |  | +    const boundElements = (
 | 
	
		
			
				|  |  | +      container.boundElements || (container.boundElements = [])
 | 
	
		
			
				|  |  | +    ).slice();
 | 
	
		
			
				|  |  | +    boundElements.push({ type: "text", id: boundElement.id });
 | 
	
		
			
				|  |  | +    container.boundElements = boundElements;
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +};
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |  export const restoreElements = (
 | 
	
		
			
				|  |  |    elements: ImportedDataState["elements"],
 | 
	
		
			
				|  |  |    /** NOTE doesn't serve for reconciliation */
 | 
	
	
		
			
				|  | @@ -242,7 +319,7 @@ export const restoreElements = (
 | 
	
		
			
				|  |  |    refreshDimensions = false,
 | 
	
		
			
				|  |  |  ): ExcalidrawElement[] => {
 | 
	
		
			
				|  |  |    const localElementsMap = localElements ? arrayToMap(localElements) : null;
 | 
	
		
			
				|  |  | -  return (elements || []).reduce((elements, element) => {
 | 
	
		
			
				|  |  | +  const restoredElements = (elements || []).reduce((elements, element) => {
 | 
	
		
			
				|  |  |      // filtering out selection, which is legacy, no longer kept in elements,
 | 
	
		
			
				|  |  |      // and causing issues if retained
 | 
	
		
			
				|  |  |      if (element.type !== "selection" && !isInvisiblySmallElement(element)) {
 | 
	
	
		
			
				|  | @@ -260,6 +337,18 @@ export const restoreElements = (
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |      return elements;
 | 
	
		
			
				|  |  |    }, [] as ExcalidrawElement[]);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  // repair binding. Mutates elements.
 | 
	
		
			
				|  |  | +  const restoredElementsMap = arrayToMap(restoredElements);
 | 
	
		
			
				|  |  | +  for (const element of restoredElements) {
 | 
	
		
			
				|  |  | +    if (isTextElement(element) && element.containerId) {
 | 
	
		
			
				|  |  | +      repairBoundElement(element, restoredElementsMap);
 | 
	
		
			
				|  |  | +    } else if (element.boundElements) {
 | 
	
		
			
				|  |  | +      repairContainerElement(element, restoredElementsMap);
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  return restoredElements;
 | 
	
		
			
				|  |  |  };
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  const coalesceAppStateValue = <
 |