pas2js/packages/rtl/webrouter.pp
2020-03-23 13:52:15 +00:00

1535 lines
37 KiB
ObjectPascal

{
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
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;
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;
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): TRoute;
function GetRoute(const Path: String; Params: TStrings): 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;
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);
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);
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): TRoute;
Var
I : Integer;
APathInfo : String;
begin
APathInfo:=SanitizeRoute(Path);
Result:=Nil;
I:=0;
While (Result=Nil) and (I<FRoutes.Count) do
begin
Result:=FRoutes[i];
If Not Result.MatchPattern(APathInfo,Params) then
Result:=Nil;
Inc(I);
end;
end;
function TRouter.GetRoute(const Path: String; Params : TStrings): TRoute;
begin
Result:=FindHTTPRoute(Path,Params);
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.