{ This file is part of the Free Component Library (FCL) Copyright (c) 2017 by the Free Pascal development team HTTPRoute: HTTP request router See the file COPYING.FPC, included in this distribution, for details about the copyright. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. } { Note: The MatchPattern routine was taken from Brook Framework's router unit, by Silvio Clecio. } {$mode objfpc} // Define this to output some debugging output { $DEFINE DEBUGROUTER } unit webrouter; interface uses Classes, SysUtils, web; Type EHTTPRoute = Class(Exception); TRawLocation = String; TScrollPoint = record X,Y : Double; end; // Forward definitions; TRouter = Class; TRoute = class; THistory = Class; TRouterClass = Class of TRouter; TRouteEvent = Reference to Procedure (URl : String; aRoute : TRoute; Params: TStrings); TTransitionResult = (trOK,trError,trAbort); THistoryKind = (hkAuto,hkAbstract,hkHash,hkHTML5); TTransitionNotifyEvent = Reference to Procedure (Sender : THistory; aLocation : String; aRoute : TRoute); TAllowTransitionEvent = Reference to Procedure (Sender : THistory; aOld, aNew : TRoute; Params : TStrings; var Allow : Boolean); { THistory } THistory = Class(TObject) Private FOnAllow: TAllowTransitionEvent; FRouter: TRouter; FOnChange : TNotifyEvent; FOnReady : TTransitionNotifyEvent; FOnError : TTransitionNotifyEvent; FCurrent : TRoute; FBase : String; function GetCurrent: TRoute; Protected procedure SetupListeners; virtual; Function doPush (location: TRawLocation) : TTransitionResult; virtual; abstract; Function doreplace (location: TRawLocation) : TTransitionResult; virtual; abstract; function doGo(N: integer): TTransitionResult; virtual; abstract; procedure ensureURL (push : boolean = false); virtual; abstract; Public Constructor Create(aRouter : TRouter); reintroduce; Constructor Create(aRouter : TRouter; aBase : String); virtual; Class Function NormalizeHash(aHash : String) : string; Procedure UpdateRoute (aRoute : TRoute); Destructor Destroy; override; Function ExpectScroll : Boolean; Function SupportsScroll : Boolean; Class function getLocation (base: string): string; Class function cleanPath(aPath : string): string; // Navigation function GetCurrentLocation: String; virtual; abstract; Function Push (location: TRawLocation) : TTransitionResult; Function Replace (location: TRawLocation) : TTransitionResult; function Go(N: integer): TTransitionResult; Function NavigateForward: TTransitionResult; Function NavigateBack: TTransitionResult; Function TransitionTo(aLocation: TRawLocation) : TTransitionResult; function ConfirmTransition(aRoute: TRoute; Params: TStrings) : TTransitionResult; Property Current : TRoute Read GetCurrent; Property Router : TRouter Read FRouter; Property OnReady : TTransitionNotifyEvent Read FOnReady Write FOnReady; Property OnError : TTransitionNotifyEvent Read FOnError Write FOnError; Property OnChange : TNotifyEvent Read FOnChange Write FOnChange; Property OnAllowTransition : TAllowTransitionEvent Read FOnAllow Write FOnAllow; property Base : String Read FBase; function Kind : THistoryKind; virtual; abstract; end; { TAbstractHistory } TAbstractHistory = Class(THistory) Private FIndex: Integer; FStack: Array of TRawLocation; procedure MaybeGrow(AIndex: Integer); Protected Function doPush (location: TRawLocation) : TTransitionResult; override; Function doReplace (location: TRawLocation) : TTransitionResult; override; function doGo(N: integer): TTransitionResult; override; Public constructor Create (router: TRouter; base: string = ''); override; function getCurrentLocation: String; override; Procedure ensureURL (Push: Boolean = False); override; function Kind : THistoryKind; override; end; { THashHistory } THashHistory = Class(THistory) Protected FlastHash : String; procedure DoHashChange; virtual; procedure SetupListeners; override; Function doPush (location: TRawLocation) : TTransitionResult; override; Function doreplace (location: TRawLocation) : TTransitionResult; override; function doGo(N: integer): TTransitionResult; override; procedure ensureURL (push : boolean = false); override; Public function getCurrentLocation: String; override; Class Procedure pushHash (path : string); Class Procedure replaceHash (path : string); class function getUrl (APath : string) : string; Class function getHash : string; function Kind : THistoryKind; override; end; { THTMLHistory } THTMLHistory = Class(THistory) Protected FlastLocation : String; procedure DoStateChange; virtual; procedure SetupListeners; override; Function doPush (location: TRawLocation) : TTransitionResult; override; Function doreplace (location: TRawLocation) : TTransitionResult; override; function doGo(N: integer): TTransitionResult; override; procedure ensureURL (push : boolean = false); override; Public function getCurrentLocation: String; override; Class Procedure pushState (path : string; doReplace : boolean = false); Class Procedure replaceState (path : string); function getUrl (ALocation : string) : string; function Kind : THistoryKind; override; end; { TRoute } TRoute = Class(TCollectionItem) private FData: TObject; FDefault: Boolean; FEvent: TRouteEvent; FURLPattern: String; procedure SetURLPattern(AValue: String); Public Class function NormalizeURLPattern(AValue: String): String; Function Matches(Const APattern : String) : Boolean; Function MatchPattern(Const Path : String; L : TStrings) : Boolean; Procedure HandleRequest(ARouter : TRouter; Const URL : String; L : TStrings); virtual; abstract; Function FullPath : String; Published Property Default : Boolean Read FDefault Write FDefault; Property URLPattern : String Read FURLPattern Write SetURLPattern; Property Event : TRouteEvent Read FEvent Write FEvent; Property Data : TObject Read FData Write FData; end; TRouteClass = Class of TRoute; { TRouteList } TRouteList = Class(TCollection) private function GetR(AIndex : Integer): TRoute; procedure SetR(AIndex : Integer; AValue: TRoute); Public Property Routes[AIndex : Integer] : TRoute Read GetR Write SetR; default; end; TRouteObject = Class(TObject) Procedure HandleRoute (Const URL : String; Params : TStrings); virtual; abstract; end; TRouteObjectClass = Class of TRouteObject; { TRouter } TBeforeRouteEvent = reference to procedure(Sender : TObject; Var ARouteURL : String); TAfterRouteEvent = reference to procedure(Sender : TObject; const ARouteURL : String); TScrollParams = Record selector : string; Position : TScrollPoint; end; TPageScrollEvent = reference to Function(Sender : TObject; aTo,aFrom : TRoute; aPosition : TScrollPoint) : TScrollParams; TRouter = Class(TComponent) Private Class Procedure DoneService; Class Var FService : TRouter; FServiceClass : TRouterClass; private FAfterRequest: TAfterRouteEvent; FBeforeRequest: TBeforeRouteEvent; FHistory: THistory; FOnScroll: TPageScrollEvent; FRoutes : TRouteList; FUseDefaultRoute: Boolean; function GetHistory: THistory; function GetHistoryKind: THistoryKind; function GetR(AIndex : Integer): TRoute; function GetRouteCount: Integer; Protected // Return an instance of given class with Pattern, Method, IsDefault filled in. function CreateHTTPRoute(AClass: TRouteClass; const APattern: String; IsDefault: Boolean ): TRoute; virtual; // Override this if you want to use another collection class. Function CreateRouteList : TRouteList; virtual; Procedure CheckDuplicate(APattern : String; isDefault : Boolean); // Actually route request. Override this for customized behaviour. function DoRouteRequest(ARoute : TRoute; Const AURL : String; AParams : TStrings) : TRoute; virtual; function DoRouteRequest(AURL : String; DoPush : Boolean = False) : TRoute; Public Constructor Create(AOwner: TComponent); override; Destructor Destroy; override; Procedure InitHistory(aKind : THistoryKind; aBase : String = ''); // Delete given route by index. Procedure DeleteRoute(AIndex : Integer); // Delete given route by index. Procedure DeleteRouteByID(AID : Integer); // Delete given route by index. The route object will be freed. Procedure DeleteRoute(ARoute : TRoute); // Sanitize route path. Strips of query parameters and makes sure it ends in / class function SanitizeRoute(const Path: String): String; // Global instance. Class Function Service : TRouter; // Class for global instance when it is created; Class Function ServiceClass : TRouterClass; // This will destroy the service Class Procedure SetServiceClass(AClass : TRouterClass); // Register event based route Function RegisterRoute(Const APattern : String; AEvent: TRouteEvent; IsDefault : Boolean = False) : TRoute;overload; // Object class based route. The router is responsible for the lifetime of the object instance Function RegisterRoute(Const APattern : String; const AObjectClass: TRouteObjectClass; IsDefault : Boolean = False) : TRoute; overload; // Find route. Matches Path on the various patterns. If a pattern is found, then the method is tested. // Returns the route that matches the pattern and method. function FindHTTPRoute(const Path: String; Params: TStrings; AllowDefault : Boolean = False): TRoute; // Call FindHTTPRoute and raise exception if no route is found. function GetRoute(const Path: String; Params: TStrings; AllowDefault : Boolean = False): TRoute; // Get Default Route. Can be Nil if there is no default route function GetDefaultRoute: TRoute; // Do actual routing. Exceptions raised will not be caught. // If DoPush is true, the URL will be pushed on the browser history. If False, the route is simply activated. Function RouteRequest(Const ARouteURL : String; DoPush : Boolean = False) : TRoute; // Extract request path from URL. By default, returns the URL function GetRequestPath(const URL: String): String; virtual; // Examine the URL hash and route the request. Returns the route. Use when arriving on a page do handle the initial route Function RouteFromURL : String; // Navigation. These are easy-access methods for history. function GetCurrentLocation: String; // These use the history mechanism Function Push (location: TRawLocation) : TTransitionResult; Function Replace (location: TRawLocation) : TTransitionResult; function Go(N: integer): TTransitionResult; Function NavigateForward : TTransitionResult; Function NavigateBack :TTransitionResult; // Indexed access to the registered routes. Property Routes [AIndex : Integer] : TRoute Read GetR; Default; // Number of registered routes. Property RouteCount : Integer Read GetRouteCount; // Events executed before and after request. In case of exception, after is not executed. Property BeforeRequest : TBeforeRouteEvent Read FBeforeRequest Write FBeforeRequest; Property AfterRequest : TAfterRouteEvent Read FAfterRequest Write FAfterRequest; // OnScroll Property OnScroll : TPageScrollEvent Read FOnScroll Write FOnScroll; // Currently used history mechanism Property History : THistory Read GetHistory; // Kind of current history. Shortcut for History.Kind, returns hkauto if History is not assigned Property HistoryKind : THistoryKind Read GetHistoryKind; // Set true to automatically go to default route if a valid route is not found (and there is a default). Property UseDefaultRoute : Boolean Read FUseDefaultRoute Write FUseDefaultRoute; end; TWebScroll = Class Class Procedure scrollToPosition (AScroll : TScrollParams); Class function getScrollPosition : TScrollPoint; Class Procedure SaveScrollPosition; Class Procedure Setup; Class Procedure handle (router: TRouter; ato: TRoute; afrom: TRoute; isPop: boolean) ; Class Function GetStateKey : String; end; TBrowserState = Class Private Class var TheKey : String; Public Class Function GenKey : String; Class Function supportsPushState : Boolean; Class function GetStateKey : string; Class Procedure SetStateKey (akey: string); Class Procedure PushState (aUrl : string; replace : boolean); Class Procedure ReplaceState(aUrl: string); end; // Shortcut for TRouter.Service; Function Router : TRouter; Function IncludeHTTPPathDelimiter (S : String) : String; implementation uses strutils, js; Resourcestring EDuplicateRoute = 'Duplicate route pattern: %s'; EDuplicateDefaultRoute = 'Duplicate default route registered with pattern: %s'; function Router: TRouter; begin Result:=TRouter.Service; end; function IncludeHTTPPathDelimiter(S: String): String; begin If (Copy(S,Length(S),1)='/') then Result:=S else Result:=S+'/'; end; { THTMLHistory } procedure THTMLHistory.DoStateChange; Var NewLocation : String; Old : TRoute; begin NewLocation:=getLocation(FBase); if (NewLocation=FLastLocation) then exit; old:=Current; if TransitionTo(NewLocation)=trOK then begin TWebScroll.Handle(router, Current, old, true); FLastLocation:=NewLocation; end else replaceState(FLastLocation); end; procedure THTMLHistory.SetupListeners; begin Window.addEventListener('popstate',@DoStateChange) end; function THTMLHistory.doPush(location: TRawLocation): TTransitionResult; begin pushState(GetURL(Location)); Result:=trOK; end; function THTMLHistory.doreplace(location: TRawLocation): TTransitionResult; begin ReplaceState(GetURL(Location)); Result:=trOK; end; function THTMLHistory.doGo(N: integer): TTransitionResult; begin window.history.go(n); Result:=trOK; end; procedure THTMLHistory.ensureURL(push: boolean); var URL,Actual,Expected : string; begin Actual:=getCurrentLocation; Expected:=FlastLocation; if (Actual<>Expected) then begin url:=getUrl(Expected); if Push then pushState(url) else replaceState(url) end; end; function THTMLHistory.getCurrentLocation: String; begin Result:=window.locationString; end; class procedure THTMLHistory.pushState(path: string; doReplace: boolean); begin TBrowserState.pushState(Path,doReplace); end; class procedure THTMLHistory.replaceState(path: string); begin pushState(Path,True); end; function THTMLHistory.getUrl(ALocation : string): string; begin Result:=IncludeHTTPPathDelimiter(FBase); While (ALocation<>'') and (Copy(ALocation,1,1)='/') do ALocation:=Copy(ALocation,2,Length(ALocation)-1); Result:=FBase+Alocation; end; function THTMLHistory.Kind: THistoryKind; begin Result:=hkHTML5; end; { THistory } function THistory.GetCurrent: TRoute; begin Result:=FCurrent; end; constructor THistory.Create(aRouter: TRouter); begin Create(aRouter,''); end; constructor THistory.Create(aRouter: TRouter; aBase: String); begin FRouter:=aRouter; FBase:=aBase; end; class function THistory.NormalizeHash(aHash: String): string; begin Result:=aHash; if Copy(Result,1,1)<>'/' then Result:='/'+Result; end; destructor THistory.Destroy; begin inherited Destroy; end; function THistory.ExpectScroll: Boolean; begin Result:=Assigned(Router) and Assigned(Router.OnScroll); end; function THistory.SupportsScroll: Boolean; begin Result:=TBrowserState.supportsPushState and ExpectScroll; end; function THistory.TransitionTo(aLocation: TRawLocation): TTransitionResult; Var Params : TStrings; R : TRoute; begin Params:=TStringList.Create; try R:=Router.FindHTTPRoute(aLocation,Params,Router.UseDefaultRoute); Case ConfirmTransition(R,Params) of trOK : begin R:=Router.DoRouteRequest(R,aLocation,Params); UpdateRoute(R); if Assigned(OnReady) then OnReady(Self,aLocation,R); end; trError: if Assigned(OnError) then FOnError(Self,aLocation,R); end; Finally Params.Free; end; Result:=trOK; end; function THistory.ConfirmTransition(aRoute: TRoute; Params : TStrings): TTransitionResult; Var Old : TRoute; allow : Boolean; begin Old:=Current; Allow:=True; if Assigned(FOnAllow) then FOnAllow(Self,old,aRoute,Params,Allow); if Not Allow then begin ensureURL(); Result:=trAbort; end; Result:=trOK; end; { TRouteObjectHandler } Type TRouteObjectHandler = Class(TRoute) private FObjectClass: TRouteObjectClass; Public Procedure HandleRequest(ARouter : TRouter; Const URL : String; Params : TStrings); override; Property RouteObjectClass : TRouteObjectClass Read FObjectClass Write FObjectClass; end; { TRouteEventHandler } TRouteEventHandler = Class(TRoute) Public Procedure HandleRequest(ARouter : TRouter; Const URL : String; Params : TStrings); override; Property Event : TRouteEvent Read FEvent Write FEvent; end; { TRouteEventHandler } procedure TRouteEventHandler.HandleRequest(ARouter : TRouter; const URL: String; Params: TStrings); begin If Assigned(Event) then Event(URL,Self,Params); end; procedure TRouteObjectHandler.HandleRequest(ARouter : TRouter; Const URL : String; Params : TStrings); Var O : TRouteObject; begin O:=RouteObjectClass.Create; try O.HandleRoute(URL,Params); finally O.Free; end; end; { TRouter } function TRouter.GetR(AIndex : Integer): TRoute; begin Result:=FRoutes[AIndex] end; function TRouter.GetHistory: THistory; begin If (FHistory=Nil) then InitHistory(hkAuto,''); Result:=FHistory; end; function TRouter.GetHistoryKind: THistoryKind; begin if Not assigned(History) then Result:=hkAuto else Result:=History.Kind; end; class procedure TRouter.DoneService; begin FreeAndNil(FService); end; function TRouter.GetRouteCount: Integer; begin Result:=FRoutes.Count; end; function TRouter.CreateRouteList: TRouteList; begin Result:=TRouteList.Create(TRoute); end; procedure TRouter.CheckDuplicate(APattern: String; isDefault: Boolean); Var I,DI : Integer; R : TRoute; begin DI:=-1; For I:=0 to FRoutes.Count-1 do begin R:=FRoutes[I]; if R.Default then DI:=I; if R.Matches(APattern) then Raise EHTTPRoute.CreateFmt(EDuplicateRoute,[APattern]); end; if isDefault and (DI<>-1) then Raise EHTTPRoute.CreateFmt(EDuplicateDefaultRoute,[APattern]); end; function TRouter.DoRouteRequest(ARoute: TRoute; const AURL: String; AParams: TStrings): TRoute; begin Result:=aRoute; if Assigned(Result) then Result.HandleRequest(Self,aURL,AParams) else Raise EHTTPRoute.CreateFmt('No route for URL: %s',[aURL]); end; function TRouter.DoRouteRequest(AURL: String; DoPush : Boolean = False): TRoute; Var APath : String; Params : TStrings; begin APath:=AURL; Params:=TStringList.Create; try Result:=GetRoute(APath,Params,UseDefaultRoute); if DoPush then Push(aURL) else Result:=DoRouteRequest(Result,aPath,Params); finally Params.Free; end; end; function TRouter.GetRequestPath(const URL: String): String; begin Result:=SanitizeRoute(URL); end; function TRouter.RouteFromURL: String; begin // Result:=Copy(window.location.hash,2,Length(window.location.hash)-1); // Writeln('Curr ', GetCurrentLocation,' route : ',Result); Result:=GetCurrentLocation; if (Result<>'') then Router.RouteRequest(Result,true); end; function TRouter.GetCurrentLocation: String; begin Result:=History.GetCurrentLocation; end; function TRouter.Push(location: TRawLocation): TTransitionResult; begin Result:=History.Push(location); end; function TRouter.Replace(location: TRawLocation): TTransitionResult; begin Result:=History.Replace(location); end; function TRouter.Go(N: integer): TTransitionResult; begin Result:=History.Go(N); end; function TRouter.NavigateForward: TTransitionResult; begin Result:=Go(1); end; function TRouter.NavigateBack: TTransitionResult; begin Result:=Go(-1); end; constructor TRouter.Create(AOwner: TComponent); begin inherited Create(AOwner); froutes:=CreateRouteList; end; destructor TRouter.Destroy; begin FreeAndNil(FRoutes); inherited Destroy; end; procedure TRouter.InitHistory(aKind: THistoryKind; aBase : String = ''); begin FreeAndNil(FHistory); case aKind of hkAbstract : FHistory:=TAbstractHistory.Create(Self,aBase); hkhash : FHistory:=THashHistory.Create(Self,aBase); hkHTML5 : FHistory:=THTMLHistory.Create(Self,aBase); hkAuto : if TBrowserState.supportsPushState then FHistory:=THTMLHistory.Create(Self,aBase) else FHistory:=THashHistory.Create(Self,aBase); end; FHistory.SetupListeners; end; procedure TRouter.DeleteRoute(AIndex: Integer); begin FRoutes.Delete(Aindex) end; procedure TRouter.DeleteRouteByID(AID: Integer); Var R : TCollectionItem; begin R:=FRoutes.FindItemID(AID); R.Free; end; procedure TRouter.DeleteRoute(ARoute: TRoute); begin ARoute.Free; end; class function TRouter.Service: TRouter; begin if FService=Nil then FService:=ServiceClass.Create(Nil); Result:=FService; end; class function TRouter.ServiceClass: TRouterClass; begin If FServiceClass=nil then FServiceClass:=TRouter; Result:=FServiceClass; end; class procedure TRouter.SetServiceClass(AClass: TRouterClass); begin if Assigned(FService) then FreeAndNil(FService); FServiceClass:=AClass; end; function TRouter.RegisterRoute(const APattern: String; AEvent: TRouteEvent; IsDefault: Boolean): TRoute; begin Result:=CreateHTTPRoute(TRouteEventHandler,APattern,IsDefault); TRouteEventHandler(Result).Event:=AEvent; end; function TRouter.CreateHTTPRoute(AClass : TRouteClass; const APattern: String;IsDefault: Boolean) : TRoute; begin CheckDuplicate(APattern,isDefault); Result:=AClass.Create(FRoutes); With Result do begin URLPattern:=APattern; Default:=IsDefault; end; end; function TRouter.RegisterRoute(const APattern: String; const AObjectClass: TRouteObjectClass; IsDefault: Boolean): TRoute; begin Result:=CreateHTTPRoute(TRouteObjectHandler,APattern,IsDefault); TRouteObjectHandler(Result).RouteObjectCLass:=AObjectClass; end; class function TRouter.SanitizeRoute(const Path: String): String; begin Result:=Path; end; function TRouter.FindHTTPRoute(const Path: String; Params : TStrings; AllowDefault : Boolean = False): TRoute; Var I : Integer; APathInfo : String; aDefault : TRoute; begin APathInfo:=SanitizeRoute(Path); Result:=Nil; aDefault:=Nil; I:=0; While (Result=Nil) and (I=0) do begin Result:=FRoutes[I]; if not Result.Default then Result:=Nil; Dec(I); end; end; function TRouter.GetRoute(const Path: String; Params: TStrings; AllowDefault: Boolean): TRoute; begin Result:=FindHTTPRoute(Path,Params,AllowDefault); if Not Assigned(Result) then Raise EHTTPRoute.Create('Not found'); end; function TRouter.RouteRequest(const ARouteURL: String; DoPush: Boolean): TRoute; Var AURL : String; begin AURL:=ARouteURL; If Assigned(FBeforeRequest) then FBeforeRequest(Self,AURL); Result:=DoRouteRequest(AURL,DoPush); If Assigned(FAfterRequest) then FAfterRequest(Self,AURL); end; { TRouteList } function TRouteList.GetR(AIndex : Integer): TRoute; begin Result:=Items[AIndex] as TRoute; end; procedure TRouteList.SetR(AIndex : Integer; AValue: TRoute); begin Items[AIndex]:=AValue; end; { TRoute } class function TRoute.NormalizeURLPattern(AValue: String): String; Var V : String; begin V:=IncludeHTTPPathDelimiter(AValue); if (V<>'/') and (V[1]='/') then Delete(V,1,1); Result:=V; end; procedure TRoute.SetURLPattern(AValue: String); Var V : String; begin V:=NormalizeURLPattern(AValue); if (FURLPattern=V) then Exit; FURLPattern:=V; end; function TRoute.Matches(const APattern: String): Boolean; begin Result:=(CompareText(URLPattern,NormalizeURLPattern(APattern))=0) end; function TRoute.MatchPattern(const Path: String; L: TStrings): Boolean; Function StartsWith(C : Char; S : String): Boolean; begin Result:=(Length(S)>0) and (S[1]=C); end; Function EndsWith(C : Char; S : String): Boolean; Var L : Integer; begin L:=Length(S); Result:=(L>0) and (S[L]=C); end; procedure ExtractNextPathLevel(var ALeft: string; var ALvl: string; var ARight: string; const ADelim: Char = '/'); var P: Integer; begin {$IFDEF DEBUGROUTER}Writeln('ExtractNextPathLevel >:',Aleft,' (',aLvl,') ',aRight);{$ENDIF} if (ALvl<>ADelim) then begin ALeft:=ALeft+ALvl; if StartsWith(ADelim,ARight) then begin ALeft:=ALeft+ADelim; Delete(ARight,1,1); end; end; P:=Pos(ADelim,ARight); if P=0 then P:=Length(ARight)+1; ALvl:=Copy(ARight,1,P-1); ARight:=Copy(ARight,P,MaxInt); {$IFDEF DEBUGROUTER} Writeln('ExtractNextPathLevel <:',Aleft,' (',aLvl,') ',aRight);{$ENDIF} end; procedure ExtractPrevPathLevel(var ALeft: string; var ALvl: string; var ARight: string; const ADelim: Char = '/'); var P,L: Integer; begin {$IFDEF DEBUGROUTER}Writeln('ExtractPrevPathLevel >:',Aleft,' (',aLvl,') ',aRight);{$ENDIF} if (ALvl<>ADelim) then begin ARight:=ALvl+ARight; L:=Length(ALeft); if EndsWith(ADelim,ALeft) then begin ARight:=ADelim+ARight; Delete(ALeft,L,1); end; end; P:=RPos(ADelim,ALeft); ALvl:=Copy(ALeft,P+1,MaxInt); ALeft:=Copy(ALeft,1,P); {$IFDEF DEBUGROUTER} Writeln('ExtractPrevPathLevel <:',Aleft,' (',aLvl,') ',aRight);{$ENDIF} end; Procedure AddParam(aName,AValue : String); begin if Assigned(L) then L.Values[aName]:=aValue; end; var APathInfo : String; APattern : String; VLeftPat, VRightPat, VLeftVal, VRightVal, VVal, VPat, VName: string; begin Result:= False; if (URLPattern='') then Exit; // Maybe empty pattern should match any path? APathInfo:=Path; APattern:=URLPattern; Delete(APattern, Pos('?', APattern), MaxInt); Delete(APathInfo, Pos('?', APathInfo), MaxInt); if StartsWith('/',APattern) then Delete(APattern,1,1); if StartsWith('/',APathInfo) then Delete(APathInfo,1,1); VLeftPat := ''; VLeftVal := ''; VPat := '/'; // init value is '/', not '' VVal := '/'; // init value is '/', not '' VRightPat := APattern; VRightVal := APathInfo; {$IFDEF DEBUGROUTER}Writeln('Check match on ',URLPattern);{$ENDIF} repeat // Extract next part ExtractNextPathLevel(VLeftPat, VPat, VRightPat); ExtractNextPathLevel(VLeftVal, VVal, VRightVal); {$IFDEF DEBUGROUTER}Writeln('Pat: ',VPat,' Val: ',VVal);{$ENDIF} if StartsWith(':',VPat) then AddParam(Copy(VPat,2,Maxint),VVal) else if StartsWith('*',VPat) then begin // *path VName := Copy(VPat, 2, MaxInt); VLeftPat := VRightPat; VLeftVal := VVal + VRightVal; VPat := '/'; // init value is '/', not '' VVal := '/'; // init value is '/', not '' VRightPat := ''; VRightVal := ''; // if AutoAddSlash ... if EndsWith('/',VLeftPat) and not EndsWith('/',VLeftVal) then Delete(VLeftPat, Length(VLeftPat), 1); repeat // Extract backwards ExtractPrevPathLevel(VLeftPat, VPat, VRightPat); ExtractPrevPathLevel(VLeftVal, VVal, VRightVal); if StartsWith(':', VPat) then begin // *path/:field AddParam(Copy(VPat,2,Maxint),VVal); end else // *path/const if not ((VPat='') and (VLeftPat='')) and (VPat<>VVal) then Exit; // Check if we already done if (VLeftPat='') or (VLeftVal='') then begin if VLeftPat='' then begin if (VName<>'') then AddParam(VName,VLeftVal+VVal); Result:=True; end; Exit; end; until False; end else // const if (VPat <> VVal) then Exit; // Check if we already done if (VRightPat='') or (VRightVal='') then begin if (VRightPat='') and (VRightVal='') then Result:=True else if (VRightPat='/') then Result := True; Exit; end; until False; end; function TRoute.FullPath: String; begin Result:=URLPattern; end; class function THistory.getLocation(base: string): string; Var path : string; begin path:=window.location.pathname; if (base<>'') and (Pos(base,path)=1) then path:=Copy(Path,Length(Base)+1,Length(Path)-Length(Base)); Result:=Path; if Result='' then Result:='/'; Result:=Result+window.location.search+window.location.hash end; class function THistory.cleanPath(aPath: string): string; begin Result:=StringReplace(aPath,'//','/',[rfReplaceAll]); end; function THistory.Push(location: TRawLocation): TTransitionResult; Var Old : TRoute; begin Old:=Current; Result:=TransitionTo(location); if Result=trOK then begin Result:=doPush(Location); if Result=trOK then TWebScroll.Handle(router, Current, Old, false) end; end; function THistory.Replace(location: TRawLocation): TTransitionResult; Var Old : TRoute; begin Old:=Current; Result:=TransitionTo(location); if Result=trOK then begin Result:=doReplace(Location); TWebScroll.Handle(Router,Current,Old,false); end; end; function THistory.Go(N: integer): TTransitionResult; begin Result:=doGo(N); end; function THistory.NavigateForward: TTransitionResult; begin Result:=Go(1); end; function THistory.NavigateBack: TTransitionResult; begin Result:=Go(-1); end; procedure THistory.SetupListeners; begin // Do nothing end; function DoScroll(Event: TEventListenerEvent): boolean; begin TWebScroll.SaveScrollPosition; Result:=True; end; Class Function TWebScroll.GetStateKey : string; begin Result:=TJSDate.New().toString; end; Class Procedure TWebScroll.Setup; begin // web.window.; window.history.replaceState(New(['key', GetStateKey]), ''); window.addEventListener('popstate',@DoScroll); end; Class Procedure TWebScroll.handle (router: TRouter; ato: TRoute; afrom: TRoute; isPop: boolean) ; Var Position : TScrollPoint; ScrollParams : TScrollParams; begin if Not Assigned(Router.OnScroll) then Exit; position:=getScrollPosition(); ScrollParams:=Router.OnScroll(Router, aTo, aFrom, position); scrollToPosition(ScrollParams); end; Var positionStore : TJSObject; Class procedure TWebScroll.saveScrollPosition; Var Key : string; begin key:=getStateKey(); if Key<>'' then positionStore.properties[key]:=New(['x',window.ScrollX,'y',window.ScrollY]); end; Class function TWebScroll.getScrollPosition : TScrollPoint; Var Key : string; O : JSValue; begin key:=getStateKey(); Result.X:=0; Result.Y:=0; if (Key<>'') then begin O:=positionStore[key]; if isObject(O) then begin Result.X:=Double(TJSOBject(O).Properties['x']); Result.Y:=Double(TJSOBject(O).Properties['y']); end; end; end; function getElementPosition (el: TJSElement; offset: TScrollPoint): TScrollPoint ; Var DocEl : TJSElement; docRect,elRect : TJSDOMRect; begin docEl:=document.documentElement; docRect := docEl.getBoundingClientRect(); elRect := el.getBoundingClientRect(); Result.x:= elRect.left - docRect.left - offset.x; Result.Y:= elRect.top - docRect.top - offset.y; end; Class Procedure TWebScroll.ScrollToPosition(AScroll : TScrollParams); Var el : TJSElement; P : TScrollPoint; begin if (AScroll.Selector<>'') then begin el:=document.querySelector(AScroll.Selector); if Assigned(el) then P:=getElementPosition(el,AScroll.Position) else P:=AScroll.Position; end else P:=AScroll.Position; Window.scrollTo(Round(P.x),Round(P.y)); end; Class function TBrowserState.genKey (): string ; begin Result:=IntToStr(TJSDate.now); end; Class function TBrowserState.getStateKey : string; begin if (TheKey='') then TheKey:=genKey; Result:=Thekey; end; Class Procedure TBrowserState.SetStateKey (akey: string); begin Thekey:=akey; end; Class Procedure TBrowserState.pushState (aurl: string; replace: boolean); Var O : TJSObject; begin TWebScroll.SaveScrollPosition; try if (Not replace) then SetStateKey(GenKey); O:=New(['key', GetStateKey()]); if replace then window.history.replaceState(o, '', aUrl) else window.history.pushState(o, '', aUrl); except if Replace then window.location.replace(aUrl) else window.location.Assign(aUrl); end; end; Class Procedure TBrowserState.replaceState(aUrl: string); begin pushState(aUrl, true) end; Class Function TBrowserState.supportsPushState : Boolean; Var UA : String; Function isB(B : String) : Boolean; begin Result:=Pos(B,UA)<>0; end; begin Result:=False; if isDefined(Window) and isDefined(Window.Navigator) then begin ua:=Window.Navigator.userAgent; Result:=Not ( IsB('Android 2.') or IsB('Android 4.0') or IsB('Mobile Safari') or IsB('Chrome') or isB('Windows Phone') ); end; If Result then Result:=isDefined(Window.history) and isDefined(Window.history); end; { --------------------------------------------------------------------- THashHistory ---------------------------------------------------------------------} procedure THashHistory.DoHashChange; Var NewHash : String; Old : TRoute; begin NewHash:=NormalizeHash(GetHash); if (NewHash=FLastHash) then exit; old:=Current; if TransitionTo(NewHash)=trOK then begin TWebScroll.Handle(router, Current, old, true); FLastHash:=NewHash; end else replaceHash(FLastHash); end; procedure THashHistory.SetupListeners; begin if SupportsScroll then TWebScroll.Setup; if TBrowserState.SupportsPushState then Window.addEventListener('popstate',@DoHashChange) else Window.addEventListener('hashchange',@DoHashChange); end; function THashHistory.doPush (location: TRawLocation) : TTransitionResult; Var L : String; begin L:=NormalizeHash(location); FLastHash:=L; pushHash(L); Result:=trOK; end; function THashHistory.doreplace(location: TRawLocation): TTransitionResult; Var L : String; begin L:=NormalizeHash(location); FLastHash:=L; replaceHash(L); Result:=trOK; end; function THashHistory.doGo(N: integer): TTransitionResult; begin Window.history.go(n); result:=trOK; end; procedure THashHistory.ensureURL (push : boolean = false); var aHash,CURL: string; begin CURL:=NormalizeHash(FLastHash); aHash:=getHash; if (aHash<>CURL) then if Push then pushHash(CURL) else replaceHash(CURL) end; function THashHistory.getCurrentLocation: String; begin Result:=getHash() end; class function THashHistory.getHash: string; Var HRef : String; Idx : Integer; begin // We can't use window.location.hash here because it's not // consistent across browsers - Firefox will pre-decode it! HRef:=window.location.href; Idx:=Pos('#',HRef); if (Idx=0) then Result:='' else Result:=Copy(HRef,Idx+1,Length(HRef)-Idx); end; function THashHistory.Kind : THistoryKind; begin Result:=hkHash; end; class function THashHistory.getUrl (APath : string) : string; Var HRef : String; Idx : Integer; begin HRef:=window.location.href; Idx:=Pos('#',HRef); if Idx=0 then Result:=HRef else Result:=Copy(HRef,1,Idx-1); Result:=Result+'#'+aPath; end; class procedure THashHistory.pushHash(path: string); begin if (TBrowserState.supportsPushState) then TBrowserState.pushState(getUrl(path),false) else window.location.hash:=path end; class procedure THashHistory.replaceHash(path: string); Var H : String; begin H:=GetHash; if (H=Path) then exit; if (TBrowserState.supportsPushState) then TBrowserState.replaceState(getUrl(path)) else window.location.replace(getUrl(path)) end; { --------------------------------------------------------------------- TAbstractHistory ---------------------------------------------------------------------} constructor TAbstractHistory.Create (router: TRouter; base: string = ''); begin Inherited; SetLength(FStack,0); FIndex:=-1; end; procedure TAbstractHistory.MaybeGrow(AIndex: Integer); begin if AIndex+1>Length(FStack) then Setlength(FStack,AIndex+1); end; function TAbstractHistory.doPush(location: TRawLocation): TTransitionResult; begin Inc(FIndex); MaybeGrow(FIndex); FStack[FIndex]:=Location; Result:=trOK; end; function TAbstractHistory.doReplace(location: TRawLocation): TTransitionResult; begin FStack[FIndex]:=Location; Result:=trOK; end; function TAbstractHistory.doGo(N: integer): TTransitionResult; Var I : Integer; Route : TRoute; begin I:=FIndex+N; if (I<0) or (I>=Length(FStack)) then Result:=trAbort else begin // Route:=FStack[i]; // Result:=confirmTransition(Route); if (Result=trOK) then begin FIndex:=0; updateRoute(Route); end; end; end; procedure THistory.UpdateRoute(aRoute: TRoute); begin FCurrent:=aRoute; if Assigned(FOnChange) then FOnChange(aRoute); end; function TAbstractHistory.getCurrentLocation: String; Var I : Integer; Route : string; begin I:=Length(FStack)-1; if (I>=0) then Route:=FStack[I] else Result:='/'; Result:=Route; end; procedure TAbstractHistory.ensureURL(Push: Boolean); begin // Noop if Push then ; end; function TAbstractHistory.Kind: THistoryKind; begin Result:=hkAbstract; end; begin positionStore:=new([]); end.