1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522 |
- {
- 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;
- // 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.GetCurrentLocation: String;
- begin
- 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.
|