Browse Source

core: Display folders in file manager

Added two dependencies for static version - doT (~3kb) and qwest (~8kb) for templates and ajax.
FileManager must be dynamic even in static version of website, so, for simplify process of displaying folders and entities created Observable and Render objects.
Folders are displaying in tree, for this purpose created RenderArrayTree object.
FilemanagerController for API now uses FilemanagerService (which will be shared between controllers).

Issues: #43, #47
master
Kamil Biały 2 years ago
parent
commit
01016a6ddb
18 changed files with 988 additions and 347 deletions
  1. +4
    -0
      package.json
  2. +4
    -0
      prepare.js
  3. +24
    -3
      public/themes/pluto/css/admin.css
  4. +37
    -10
      public/themes/pluto/src.static/Application.ts
  5. +0
    -243
      public/themes/pluto/src.static/FileManager.ts
  6. +2
    -2
      public/themes/pluto/src.static/Prototypes.ts
  7. +201
    -0
      public/themes/pluto/src.static/controls/FileManager.ts
  8. +230
    -0
      public/themes/pluto/src.static/extra/ObservableArray.ts
  9. +124
    -0
      public/themes/pluto/src.static/extra/RenderArray.ts
  10. +207
    -0
      public/themes/pluto/src.static/extra/RenderArrayTree.ts
  11. +19
    -0
      public/themes/pluto/src.static/interfaces/IEntity.ts
  12. +17
    -0
      public/themes/pluto/src.static/interfaces/IObservable.ts
  13. +21
    -0
      public/themes/pluto/src.static/interfaces/IRender.ts
  14. +8
    -1
      public/themes/pluto/src.static/tsconfig.json
  15. +29
    -15
      pulsar/admin/views/pluto/filemanager/index.volt
  16. +41
    -62
      pulsar/micro/FilemanagerController.php
  17. +12
    -10
      pulsar/services/FilemanagerService.php
  18. +8
    -1
      tsconfig.static.json

+ 4
- 0
package.json View File

@@ -19,7 +19,11 @@
},
"homepage": "https://pulsar.aculo.pl",
"dependencies": {
"@types/dot": "^1.1.2",
"@types/qwest": "^1.7.28",
"dot": "^1.1.2",
"font-awesome": "^4.7.0",
"qwest": "^4.5.0",
"sequelize": "^4.14.0",
"uglify-js": "^3.1.3"
},

+ 4
- 0
prepare.js View File

@@ -32,6 +32,10 @@ let font_files = [
"node_modules/font-awesome/fonts/fontawesome-webfont.woff2"
];
let js_files = [
"node_modules/dot/doT.js",
"node_modules/dot/doT.min.js",
"node_modules/qwest/src/qwest.js",
"node_modules/qwest/qwest.min.js"
// "node_modules/requirejs/require.js",
// "node_modules/requirejs/require.min.js"
];

+ 24
- 3
public/themes/pluto/css/admin.css View File

@@ -974,7 +974,7 @@ form .button-container {
}

.filemanager .caret {
width: 2.0rbem;
width: 2.0rem;
text-align: center;
font-size: 1.3rem;
line-height: 2rem;
@@ -995,10 +995,14 @@ form .button-container {
}
.dir-entry p:hover {
background: #ddd;
cursor: pointer;
cursor: default;
}
.dir-entry p:hover .folder-icon {
color: #127899;
}
.dir-entry p:hover i {
.dir-entry p i.caret:hover {
color: #127899;
cursor: pointer;
}


@@ -1039,3 +1043,20 @@ form .button-container {
.menu-entry:hover .action-container {
display: block !important;
}

.dir-entry p i {
line-height: 2rem;
}
.dir-entry p i.invisible {
visibility: hidden;
}

.directory-panel {
flex: 1;
overflow: auto;
}
.folder-icon {
width: 1.8rem;
padding: 0 !important;
color: #333;
}

+ 37
- 10
public/themes/pluto/src.static/Application.ts View File

@@ -13,15 +13,25 @@
* this program. If not, see <http://www.licenses.aculo.pl/>.
*/

const doTRegex = [
/\<\%([\s\S]+?)\%\>/g,
/\<\%=([\s\S]+?)\%\>/g,
/\<\%!([\s\S]+?)\%\>/g,
/\<\%#([\s\S]+?)\%\>/g,
/\<\%##\s*([\w\.$]+)\s*(\:|=)([\s\S]+?)#\%\>/g,
/\<\%\?(\?)?\s*([\s\S]*?)\s*\%\>/g,
/\<\%~\s*(?:\%\>|([\s\S]+?)\s*\:\s*([\w$]+)\s*(?:\:\s*([\w$]+))?\s*\%\>)/g
];

/**
* Wymagania przeglądarkowe dla statycznej wersji strony:
*
* IE: 11
* IE: 11 (partial)
* EDGE: 12
* FF: 6
* CHROME: 8
* SAFARI: 5.1
* OPERA: 11.5
* FF: 12 (6 partial)
* CHROME: 31 (8 partial)
* SAFARI: 8 (5.1 partial)
* OPERA: 18 (blink), 12.1 (presto)
*/
class Application
{
@@ -33,6 +43,23 @@ class Application
this.initCheckBoxes();
this.initTabControls();
this.initConfirmMessages();

// biblioteka doT jest wczytywana tylko gdy istnieje menedżer plików
try
{
doT.templateSettings.evaluate = doTRegex[0];
doT.templateSettings.interpolate = doTRegex[1];
doT.templateSettings.encode = doTRegex[2];
doT.templateSettings.use = doTRegex[3];
doT.templateSettings.define = doTRegex[4];
doT.templateSettings.conditional = doTRegex[5];
doT.templateSettings.iterate = doTRegex[6];

this.initFileManager();
}
catch( ex ) {
// w każdym razie brak zmiennej doT nie jest błędem
}
}

/**
@@ -81,13 +108,13 @@ class Application
*/
public initFileManager(): void
{
// const fmgrdiv = <HTMLElement>document.querySelector( ".filemanager" );
const fmgrdiv = <HTMLElement>document.querySelector( ".filemanager" );

// if( fmgrdiv == null )
// return;
if( fmgrdiv == null )
return;

// const filemanager = new FileManager( fmgrdiv );
// filemanager.addEvents();
const filemanager = new FileManager( fmgrdiv );
filemanager.addEvents();
}

/**

+ 0
- 243
public/themes/pluto/src.static/FileManager.ts View File

@@ -1,243 +0,0 @@
/*
* This file is part of Pulsar CMS
* Copyright (c) by sobiemir <sobiemir@aculo.pl>
* ___ __
* / _ \__ __/ /__ ___ _____
* / ___/ // / (_-</ _ `/ __/
* /_/ \_,_/_/___/\_,_/_/
*
* This source file is subject to the New BSD License that is bundled
* with this package in the file LICENSE.txt.
*
* You should have received a copy of the New BSD License along with
* this program. If not, see <http://www.licenses.aculo.pl/>.
*/

class FileManager
{
/**
* Główny element na którym opiera się menedżer plików.
*
* TYPE: HTMLElement
*/
private _filemgr: HTMLElement = null;
private _directories: NodeListOf<HTMLElement> = null;


// =============================================================================

/**
* Konstruktor kontrolki.
*
* PARAMETERS:
* tab: (HTMLElement)
* Główny element na którym opiera się menedżer plików.
*/
public constructor( div: HTMLElement )
{
this._filemgr = div;
this._directories = <NodeListOf<HTMLElement>>
this._filemgr.querySelectorAll( ".directory-panel" );

// // element główny z którego bedą pobierane dane
// this._form = tab.dataset.form !== undefined
// ? document.querySelector( tab.dataset.form ) as HTMLElement
// : document;

// if( !this._form )
// {
// this._failed = true;
// return;
// }

// // wiadomość w przypadku gdy model jest oznaczony jako nieistniejący
// if( tab.dataset.message !== undefined )
// {
// this._message = <HTMLElement>
// this._form.querySelector( tab.dataset.message );
// this._flags = <NodeListOf<HTMLInputElement>>
// this._form.querySelectorAll( "input[data-mflag" );
// }
// else
// {
// this._message = null;
// this._flags = null;
// }

// // przycisk usuwania danych z zakładki
// if( tab.dataset.remove !== undefined )
// this._remove = <HTMLElement>
// this._form.querySelector( tab.dataset.remove );
// // przycisk dodawania danych do zakładki
// if( tab.dataset.create !== undefined )
// this._create = <HTMLElement>
// this._form.querySelector( tab.dataset.create );

// // element w którym wyszukiwane są warianty
// this._search = <HTMLElement>
// this._form.querySelector( tab.dataset.search );

// if( !this._search )
// {
// this._failed = true;
// return;
// }

// // lista wariantów
// this._variants = <NodeListOf<HTMLElement>>
// this._search.querySelectorAll( "[data-variant]" );

// if( !this._variants || this._variants.length === 0 )
// this._failed = true;

// // lista zakładek w kontrolce
// this._tabs = <NodeListOf<HTMLLIElement>>tab.querySelectorAll( "li" );

// if( !this._tabs || this._tabs.length === 0 )
// this._failed = true;
}

/**
* Pobiera wartość parametru o podanym indeksie.
*
* PARAMETERS:
* index: (string)
* Indeks z którego wartość parametru ma zostać pobrana.
*
* RETURNS: (any)
* Wartość parametru o podanym indeksie.
*/
public get( index: string ): any
{
const name = `_${index}`;

if( name in this )
return (<any>this)[name];

return null;
}

/**
* Dodaje zdarzenia do kontrolki.
*
* DESCRIPTION:
* Kontrolka oprócz przełączania kontekstu i zakładek obsługuje również
* inne akcje, jak np. czyszczenie lub ustawianie flagi modelu.
* Szczegóły dotyczące tego jak działają te opcje i jak je ustawić
* znajdują się w opisie klasy.
*/
public addEvents(): void
{
// if( this._failed )
// return;

// this._control.addEventListener( "click", this._onTabChange );

// // te dwa przyciski wiążą się ze sobą, więc muszą być razem
// if( !this._create || !this._remove )
// return;

// this._create.addEventListener( "click", this._onCreateLanguage );
// this._remove.addEventListener( "click", this._onRemoveLanguage );
}

/**
* Zmienia zaznaczoną zakładkę w kontrolce.
*
* PARAMETERS:
* index: (number)
* Indeks zakładki która ma być zaznaczona.
* force: (boolean) = false
* Wymuszenie odświeżenia gdy indeks jest taki sam jak aktualny.
*/
public selectTab( index: number, force: boolean = false ): void
{
// // sprawdź czy zakładka nie jest czasem już zaznaczona
// if( this._failed || (!force && this._selected === index)
// || !(index in this._tabs) )
// return;

// const newli = this._tabs[index];
// const newid = newli.dataset.id;
// const oldid = this._selected !== -1
// ? this._tabs[this._selected].dataset.id
// : null;

// // zaznacz odpowiednią zakładkę
// if( oldid )
// this._tabs[this._selected].classList.remove( "selected" );
// newli.classList.add( "selected" );

// // pokaż pola przypisane do zakładki
// for( let x = 0; x < this._variants.length; ++x )
// {
// const variant = this._variants[x];

// if( variant.dataset.variant == newid )
// variant.classList.remove( "hidden" );
// else if( variant.dataset.variant == oldid )
// variant.classList.add( "hidden" );
// }

// // razem ze zmianą zakładki zmienia się też tryb wyświetlania danych
// if( this._message != null && this._flags.length > 0 )
// {
// // nazwa elementu do sprawdzenia
// const fname = `flag:${newid}`;

// // przeszukuj wszystkie flagi
// for( let x = 0; x < this._flags.length; ++x )
// {
// const flag = this._flags[x];

// if( flag.name == fname && this._remove && this._create )
// {
// // i jeżeli flaga jest dopuszczona do widoku, wyświetl dane
// if( flag.value == "1" )
// {
// this._message.classList.add( "hidden" );
// this._search.classList.remove( "hidden" );
// this._remove.classList.remove( "hidden" );
// this._create.classList.add( "hidden" );
// }
// // w przeciwnym wypadku wyświetl komunikat
// else
// {
// this._message.classList.remove( "hidden" );
// this._search.classList.add( "hidden" );
// this._remove.classList.add( "hidden" );
// this._create.classList.remove( "hidden" );
// }
// break;
// }
// }
// }
// this._selected = index;
}

// =============================================================================

/**
* Akcja wywoływana podczas zmiany zakładki w kontrolce.
*
* PARAMETERS:
* ev: (MouseEvent)
* Argumenty zdarzenia.
*/
private _onTabChange = ( ev: MouseEvent ): void =>
{
// if( this._failed )
// return;

// const newli = <HTMLElement>ev.target;
// if( newli.tagName != "LI" )
// return;

// // wyszukaj indeks nowego elementu
// const newidx = this._tabs.findIdxByFunc( (elem: HTMLElement) =>
// elem.dataset.id == newli.dataset.id
// );

// this.selectTab( newidx );
}
}

+ 2
- 2
public/themes/pluto/src.static/Prototypes.ts View File

@@ -18,10 +18,10 @@ interface NodeList
findIdxByFunc( func: (elem: any) => boolean ): number;
}

NodeList.prototype.findIdxByFunc = function( f: (e: any) => boolean): number
NodeList.prototype.findIdxByFunc = function( func: (e: any) => boolean): number
{
for( let x = 0; x < this.length; ++x )
if( f(this[x]) )
if( func(this[x]) )
return x;
return -1;
};

+ 201
- 0
public/themes/pluto/src.static/controls/FileManager.ts View File

@@ -0,0 +1,201 @@
/*
* This file is part of Pulsar CMS
* Copyright (c) by sobiemir <sobiemir@aculo.pl>
* ___ __
* / _ \__ __/ /__ ___ _____
* / ___/ // / (_-</ _ `/ __/
* /_/ \_,_/_/___/\_,_/_/
*
* This source file is subject to the New BSD License that is bundled
* with this package in the file LICENSE.txt.
*
* You should have received a copy of the New BSD License along with
* this program. If not, see <http://www.licenses.aculo.pl/>.
*/

class FileManager
{
/**
* Główny element na którym opiera się menedżer plików.
*
* TYPE: HTMLElement
*/
private _fileManager: HTMLElement = null;
private _directoryPanel: HTMLElement = null;
private _directories: RenderArrayTree<IFolder> = null;
private _entities: RenderArray<IEntity> = null;
private _entityPanel: HTMLElement = null;
private _entityTemplate: string = '';
private _directoryTemplate: string = '';


// =============================================================================

/**
* Konstruktor kontrolki.
*
* PARAMETERS:
* tab: (HTMLElement)
* Główny element na którym opiera się menedżer plików.
*/
public constructor( div: HTMLElement )
{
this._directories = new RenderArrayTree();
this._entities = new RenderArray();
this._fileManager = div;

this._directoryPanel = <HTMLElement>
this._fileManager.querySelector( ".directory-tree" );
this._entityPanel = <HTMLElement>
this._fileManager.querySelector( ".entity-panel" );

const etpl = this._fileManager.querySelector( "#tpl-entity-item" );
this._entityTemplate = etpl
? etpl.innerHTML
: '';
const dtpl = this._fileManager.querySelector( "#tpl-directory-item" );
this._directoryTemplate = dtpl
? dtpl.innerHTML
: '';

this._directories.options({
treeSelector: ".directory-subtree",
childIndex: "children",
template: this._directoryTemplate,
place: this._directoryPanel
});

this._directories.subscribe( obs =>
{
const observables = obs.getObservables();

for( const observable of observables )
{
if( !observable.wasUpdated )
continue;

const first = <HTMLElement>observable.element.firstChild;
const elements = <NodeListOf<HTMLElement>>
first.querySelectorAll( "[data-click]" );

elements.forEach( val => {
if( val.dataset.click == "toggle" )
val.addEventListener( "click", () => {
const last = <HTMLElement>observable.element.lastChild;
const i2 = val.parentNode.childNodes[1] as HTMLElement;

console.log( i2 );

if( observable.value.rolled )
{
last.classList.remove( "hidden" );
val.classList.remove( "fa-angle-right" );
val.classList.add( "fa-angle-down" );
i2.classList.remove( "fa-folder" );
i2.classList.add( "fa-folder-open" );
}
else
{
last.classList.add( "hidden" );
val.classList.remove( "fa-angle-down" );
val.classList.add( "fa-angle-right" );
i2.classList.remove( "fa-folder-open" );
i2.classList.add( "fa-folder" );
}

observable.value.rolled = !observable.value.rolled;
} );
} );
}

}, "click" );

this._entities.options({
single: false,
template: this._entityTemplate,
place: this._entityPanel
});

this.getFolders();
this.getEntities();
}

public getFolders(): Qwest.Promise
{
return qwest.get( "/micro/filemanager/directories/1" )
.then( (xhr: XMLHttpRequest, response?: IFolder[]): any => {
const noroll = (val: IFolder): void =>
{
val.rolled = true;
if( val.children && val.children.length )
val.children.forEach( noroll );
};
response.forEach( noroll );

this._directories.set( response );
} ).catch( (error: Error, xhr?: XMLHttpRequest): any => {
this._onLoadError( error, xhr );
} );
}

public getEntities( folder: string = '/' ): Qwest.Promise
{
return qwest.get( "/micro/filemanager/entities" + folder )
.then( (xhr: XMLHttpRequest, response?: IEntity[]): any => {
this._entities.set( response );
} ).catch( (error: Error, xhr?: XMLHttpRequest): any => {
this._onLoadError( error, xhr );
} );
}

/**
* Pobiera wartość parametru o podanym indeksie.
*
* PARAMETERS:
* index: (string)
* Indeks z którego wartość parametru ma zostać pobrana.
*
* RETURNS: (any)
* Wartość parametru o podanym indeksie.
*/
public get( index: string ): any
{
const name = `_${index}`;

if( name in this )
return (<any>this)[name];

return null;
}

/**
* Dodaje zdarzenia do kontrolki.
*
* DESCRIPTION:
* Kontrolka oprócz przełączania kontekstu i zakładek obsługuje również
* inne akcje, jak np. czyszczenie lub ustawianie flagi modelu.
* Szczegóły dotyczące tego jak działają te opcje i jak je ustawić
* znajdują się w opisie klasy.
*/
public addEvents(): void
{
// if( this._failed )
// return;

// this._control.addEventListener( "click", this._onTabChange );

// // te dwa przyciski wiążą się ze sobą, więc muszą być razem
// if( !this._create || !this._remove )
// return;

// this._create.addEventListener( "click", this._onCreateLanguage );
// this._remove.addEventListener( "click", this._onRemoveLanguage );
}

// =============================================================================

private _onLoadError = ( error: Error, xml: XMLHttpRequest ): void =>
{

}
}

+ 230
- 0
public/themes/pluto/src.static/extra/ObservableArray.ts View File

@@ -0,0 +1,230 @@

class ObservableArray<TYPE>
{
/**
* Lista obserwowanych wartości z których tworzone są elementy.
*
* TYPE: IObservableValue<TYPE>[]
*/
protected _values: IObservableValue<TYPE>[];

/**
* Lista połączonych funkcji wywoływanych przy odświeżaniu elementów.
*
* TYPE: IObservableFunction<TYPE>[]
*/
protected _subscribers: IObservableFunction<TYPE>[];

// =============================================================================

/**
* Konstruktor klasy.
*/
public constructor()
{
this._values = [];
this._subscribers = [];
}

/**
* Dodaje funkcję do funkcji powiązanych, wywoływanych przy aktualizacji.
*
* DESCRIPTION:
* Wiele funkcji może znajdować się pod jednym indeksem, dzięki temu
* wywołanie funkcji unsubscribe zwalnia wszystkie funkcje zapisane
* do danego indeksu.
* Funkcja wywoływana jest w takiej kolejności w jakiej została dodana,
* nie ma więc znaczenia czy dodana została do indeksu którego funkcja
* dodana została wcześniej.
*
* PARAMETERS:
* funct: Funkcja wywoływana przy aktualizacji tablicy.
* index: Indeks pod którym zapisana będzie funkcja.
*/
public subscribe( funct: TObservableArrayFunc<TYPE>, index: string ): void
{
this._subscribers.push( {
name: index,
func: funct
} );
}

/**
* Zwalnia funkcje aktualizacji przypisane do danego indeksu.
*
* PARAMETERS:
* index: string
* Indeks do którego zapisana zostanie funkcja.
*/
public unsubscribe( index: string ): void
{
let idx = -1;
do
{
idx = this._subscribers.findIndex( val => val.name == index);
if( idx > -1 )
this._subscribers.splice( idx, 1 );
}
while( idx > -1 );
}

/**
* Pobiera tablicę elementów które zostały wstawione do klasy.
*
* RETURNS: TYPE[]
* Tablicę elementów które zawiera klasa.
*/
public get(): TYPE[]
{
return this._values.map( val => val.value );
}

/**
* Pobiera tablicę obserwowanych wartości.
*
* RETURNS: IObservableValue<TYPE>[]
* Tablicę obserwowanych wartości.
*/
public getObservables(): IObservableValue<TYPE>[]
{
return this._values;
}

/**
* Ustawia nowe elementy w tablicy.
*
* PARAMETERS:
* values: TYPE[]
* Tablica elementów do wstawienia.
*/
public set( values: TYPE[] ): void
{
this._values = values.map( val => this._valueConverter(
{
element: null,
value: val,
needUpdate: true,
wasUpdated: false,
extra: this
}) );
this.runSubscribers();
}

/**
* Czyści tablicę z wszystkich elementów.
*/
public clear(): void
{
this._values.length = 0;
this.runSubscribers();
}

/**
* Odwraca elementy w tablicy.
*/
public reverse(): void
{
this._values.reverse();
this.runSubscribers();
}

/**
* Wstawia do tablicy na koniec podane wartości.
*
* PARAMETERS:
* vals: ...TYPE[]
* Lista wartości do wstawienia.
* RETURNS: number
* Ilość elementów w tablicy po wstawieniu.
*/
public push( ...vals: TYPE[] ): number
{
const num = this._values.push( ...vals.map(val => this._valueConverter(
{
element: null,
value: val,
needUpdate: true,
wasUpdated: false,
extra: this
})) );
this.runSubscribers();
return num;
}

/**
* Usuwa ostatnią wartość z tablicy, zwracając ją.
*
* RETURNS:
* Usuniętą wartość z tablicy.
*/
public pop(): TYPE
{
const v = this._values.pop();
this.runSubscribers();

return v.value;
}

/**
* Dodaje do tablicy na początek podane wartości.
*
* PARAMETERS:
* vals: ...TYPE[]
* Lista wartości do wstawienia.
* RETURNS: number
* Ilość elementów w tablicy po wstawieniu.
*/
public unshift( ...vals: TYPE[] ): number
{
const n = this._values.unshift( ...vals.map(val => this._valueConverter(
{
element: null,
value: val,
needUpdate: true,
wasUpdated: false,
extra: this
})) );
this.runSubscribers();
return n;
}

/**
* Usuwa pierwszą wartość z tablicy, zwracając ją.
*
* RETURNS:
* Usuniętą wartość z tablicy.
*/
public shift(): TYPE
{
const v = this._values.shift();
this.runSubscribers();

return v.value;
}

/**
* Uruchamia wszystkie funkcje powiązane z obserwowaną tablicą.
*/
public runSubscribers(): void
{
for( let x = 0; x < this._subscribers.length; ++x )
this._subscribers[x].func( this );
}

// =============================================================================

/**
* Pozwala na konwersję wartości do innego obiektu niż oryginalny.
*
* PARAMETERS:
* values: IObservableValue<TYPE>
* Wartości do zamiany na inną tablicę.
* RETURNS: IObservable
* Tablicę z nowymi wartościami, tutaj przekazywana w parametrze.
*/
protected _valueConverter( values: IObservableValue<TYPE> ):
IObservableValue<TYPE>
{
return values;
}
}

+ 124
- 0
public/themes/pluto/src.static/extra/RenderArray.ts View File

@@ -0,0 +1,124 @@

class RenderArray<TYPE> extends ObservableArray<TYPE>
{
/**
* Szablon skompilowany przez bibliotekę doT.
*
* TYPE: doT.RenderFunction
*/
protected _template: doT.RenderFunction;

/**
* Miejsce do którego elementy będą wrzucane.
*
* TYPE: HTMLElement
*/
protected _place: HTMLElement;

/**
* Czy renderować jeden szablon dla wszystkich elementów?
*
* TYPE: boolean
*/
protected _single: boolean;

// =============================================================================

/**
* Konstruktor klasy.
*/
public constructor()
{
super();

this._template = null;
this._place = null;
this._single = false;
}

/**
* Możliwe do ustawienia opcje dla klasy RenderArray.
*
* DESCRIPTION:
* Opcje warto ustawić jeszcze przed wstawianiem elementów do tablicy.
* Zmiana opcji spowoduje odświeżenie widoku elementów.
*
* PARAMETERS:
* options: IRenderArrayOptions
* Lista opcji do zmiany.
*/
public options( options: IRenderArrayOptions ): void
{
if( options.single )
this._single = options.single;
if( options.place )
this._place = options.place;
if( options.template )
this._template = typeof options.template == "function"
? options.template
: doT.template( options.template );

this.runSubscribers();
}


/**
* Uruchamia wszystkie funkcje powiązane z obserwowaną tablicą.
*/
public runSubscribers(): void
{
if( !this._place || !this._template )
return;

// tryb pojedynczego szablonu - wszystkie na raz
if( this._single )
this._place.innerHTML = this._template( this._values );
// wszystkie osobno - szablon dla każdego elementu
else
{
// usuń wszystkie dzieci
while( this._place.lastChild )
this._place.removeChild( this._place.lastChild );

this._render();
}

super.runSubscribers();
}

// =============================================================================

/**
* Renderuje elementy z obserwowanej tablicy w aplikacji.
*
* DESCRIPTION:
* Uruchomienie tej funkcji spowoduje aktualizację widoku w elemencie
* do którego podpięta jest klasa RenderArray.
* W przypadku gdy generowane są wszystkie elementy na raz (aktywna
* opcja single), żaden z wyrenderowanych elementów nie ma odnośnika.
* Niemożliwe jest więc prawidłowe przeskanowanie wartości które
* elementy się zmieniły, więc subskrypcje stają się wtedy nieco
* bezużyteczne.
*/
protected _render(): void
{
for( const value of this._values )
{
value.wasUpdated = false;

// sprawdź czy element wymaga aktualizacji
if( value.needUpdate )
{
const div = document.createElement( "div" );
div.innerHTML = this._template( value.value );
value.element = div.firstChild as HTMLElement;

value.needUpdate = false;
value.wasUpdated = true;
}

// dodaj element do rodzica
value.extra._place.appendChild( value.element );
}
}
}

+ 207
- 0
public/themes/pluto/src.static/extra/RenderArrayTree.ts View File

@@ -0,0 +1,207 @@

class RenderArrayTree<TYPE> extends ObservableArray<TYPE>
{
/**
* Szablon skompilowany przez bibliotekę doT.
*
* TYPE: doT.RenderFunction
*/
protected _template: doT.RenderFunction;

/**
* Miejsce do którego elementy będą wrzucane.
*
* TYPE: HTMLElement
*/
protected _place: HTMLElement;

/**
* Nazwa selektora w przypadku renderowania drzewa elementów.
*
* TYPE: string
*/
protected _tree: string;

/**
* Indeks używany przy dostępie do dzieci w drzewie.
*
* TYPE: string
*/
protected _index: string;

/**
* Rodzic tablicy w przypadku tworzenia drzewa.
*
* TYPE: RenderArray<TYPE>
*/
protected _parent: RenderArrayTree<TYPE>;

/**
* Wartość względem której podpinany jest rodzic.
*
* TYPE: IObservableValue<TYPE>
*/
protected _upper: IObservableValue<TYPE>;

// =============================================================================

/**
* Konstruktor klasy.
*/
public constructor(
parent: RenderArrayTree<TYPE> = null,
upper: IObservableValue<TYPE> = null
) {
super();

this._template = null;
this._place = null;
this._tree = null;
this._index = '';
this._parent = parent;
this._upper = upper;
}

public options( options: IRenderArrayTreeOptions ): void
{
if( options.childIndex )
this._index = options.childIndex;
if( options.treeSelector )
this._tree = options.treeSelector;
if( options.place )
this._place = options.place;
if( options.template )
this._template = typeof options.template == "function"
? options.template
: doT.template( options.template );
}

public getParent(): RenderArrayTree<TYPE>
{
return this._parent;
}

public getUpper(): IObservableValue<TYPE>
{
return this._upper;
}

/**
* Renderuje elementy z obserwowanej tablicy w aplikacji.
*
* DESCRIPTION;
* Uruchomienie tej funkcji spowoduje aktualizację widoku w elemencie
* do którego podpięta jest klasa Observable.
* W przypadku gdy generowane są wszystkie elementy na raz (aktywna
* opcja single), żaden z wyrenderowanych elementów nie ma odnośnika.
* Niemożliwe jest więc prawidłowe przeskanowanie wartości które
* elementy się zmieniły, więc subskrypcje stają się wtedy nieco
* bezużyteczne.
*/
public runSubscribers(): void
{
if( !this._place || !this._template || !this._index || !this._tree )
return;

// usuń wszystkie dzieci
while( this._place.lastChild )
this._place.removeChild( this._place.lastChild );

this._render();

super.runSubscribers();
}

// =============================================================================

/**
* Renderuje elementy z obserwowanej tablicy w aplikacji.
*
* DESCRIPTION:
* Funkcja wywołuje samą siebie w przypadku gdy włączona została opcja
* generowania elementów w drzewie.
* Aby wygenerować drzewo należy wcześniej podać selektor dla elementu
* w którym będą generowane podelementy.
*
* PARAMETERS:
* values: IRenderElement<TYPE>[]
* Obserwowane wartości w tablicy.
*/
protected _render(): void
{
for( const value of this._values )
{
value.wasUpdated = false;

// sprawdź czy element wymaga aktualizacji
if( value.needUpdate )
{
const div = document.createElement( "div" );
div.innerHTML = this._template( value.value );
value.element = div.firstChild as HTMLElement;

value.needUpdate = false;
value.wasUpdated = true;
}
const extra = value.extra as IRenderTreeExtra<TYPE>;

// dodaj element do rodzica
extra.owner._place.appendChild( value.element );

if( !extra.child )
continue;

// zapisz miejsce generowania elementów do obserwowanej tablicy
extra.child.options( {
place: <HTMLElement>value.element.querySelector( this._tree )
} );

extra.child.runSubscribers();
}
}

/**
* Tworzy nową obserwowaną tablicę z podelementów aktualnego elementu.
*
* PARAMETERS:
* values: TYPE[]
* Tablica zawierająca podelementy.
* parent: RenderArray<TYPE>
* Tablica nadrzędna, która jest rodzicem elementów.
*
* RETURNS: RenderArray<TYPE>
* Tablicę zawierającą obserwowane wartości będącą podtablicą aktualnej.
*/
protected _valueConverter( obs: IObservableValue<TYPE> ):
IObservableValue<TYPE>
{
const val = {
element: obs.element,
value: obs.value,
needUpdate: obs.needUpdate,
wasUpdated: obs.wasUpdated,
extra: {
owner: this,
child: null
} as IRenderTreeExtra<TYPE>
} as IObservableValue<TYPE>;

if( !(obs.value as any)[this._index] ||
!(obs.value as any)[this._index].length )
return val;

const observable = new RenderArrayTree<TYPE>( this, val );
observable.options(
{
childIndex: this._index,
treeSelector: this._tree,
place: this._place,
template: this._template
} );
observable._subscribers = this._subscribers;
observable.set( (obs.value as any)[this._index] );

val.extra.child = observable;
return val;
}
}

+ 19
- 0
public/themes/pluto/src.static/interfaces/IEntity.ts View File

@@ -0,0 +1,19 @@

interface IEntity
{
name: string;
size: number;
modify: string;
access: string;
type: string;
mime: string;
}

interface IFolder
{
name: string;
modify: string;
access: string;
children: IFolder[];
rolled: boolean;
}

+ 17
- 0
public/themes/pluto/src.static/interfaces/IObservable.ts View File

@@ -0,0 +1,17 @@

interface IObservableValue<TYPE>
{
element: HTMLElement;
value: TYPE;
needUpdate: boolean;
wasUpdated: boolean;
extra: any;
}

interface IObservableFunction<TYPE>
{
name: string;
func: TObservableArrayFunc<TYPE>;
}

type TObservableArrayFunc<TYPE> = (obs: ObservableArray<TYPE>) => void;

+ 21
- 0
public/themes/pluto/src.static/interfaces/IRender.ts View File

@@ -0,0 +1,21 @@

interface IRenderTreeExtra<TYPE>
{
owner?: RenderArrayTree<TYPE>;
child?: RenderArrayTree<TYPE>;
}

interface IRenderArrayOptions
{
single?: boolean;
template?: string | doT.RenderFunction;
place?: HTMLElement;
}

interface IRenderArrayTreeOptions
{
treeSelector?: string;
template?: string | doT.RenderFunction;
place?: HTMLElement;
childIndex?: string;
}

+ 8
- 1
public/themes/pluto/src.static/tsconfig.json View File

@@ -1,7 +1,7 @@
{
"compilerOptions": {
"module": "None",
"target": "ES5",
"target": "es2015",
"noImplicitAny": true,
"removeComments": true,
"preserveConstEnums": true,
@@ -10,8 +10,15 @@
"outFile": "../js/static.js"
},
"files": [
"interfaces/IEntity.ts",
"interfaces/IObservable.ts",
"interfaces/IRender.ts",
"extra/ObservableArray.ts",
"extra/RenderArray.ts",
"extra/RenderArrayTree.ts",
"controls/CheckBox.ts",
"controls/TabControl.ts",
"controls/FileManager.ts",
"Application.ts",
"Prototypes.ts"
]

+ 29
- 15
pulsar/admin/views/pluto/filemanager/index.volt View File

@@ -1,18 +1,5 @@
{%- macro display_directories( dirs ) %}
<ul class="hidden">
{%- for dir, subdirs in dirs %}
<li class="dir-entry">
<p>
<i class="fa fa-caret-right caret"></i>
<span>{{ dir }}</span>
</p>
{%- if subdirs is not empty %}
{{ display_directories( subdirs ) }}
{%- endif %}
</li>
{%- endfor %}
</ul>
{%- endmacro %}
{{ javascript_include("vendors/js/doT.min") }}
{{ javascript_include("vendors/js/qwest.min") }}

<div class="items-vertical fill-free">
<div class="head-bar mb00">
@@ -26,6 +13,8 @@
<i class="fa fa-refresh"></i>
</div>
<div class="directory-panel fill-free p05">
<ul class="directory-tree">
</ul>
</div>
</aside>
<div class="fill-free">
@@ -36,6 +25,31 @@
<i class="fa fa-folder"></i>
<i class="fa fa-search"></i>
</div>
<div class="entity-panel fill-free p05">
</div>
</div>

<script type="text/template" id="tpl-entity-item">
<ul>
<%~ it :value %>
<li>
<%= value.name %>
</li>
<%~%>
</ul>
</script>
<script type="text/template" id="tpl-directory-item">
<li class="dir-entry">
<div>
<p data-click="browse">
<i class="fa fa-angle-right caret <%? !it.children.length %>invisible<%?%>" data-click="toggle"></i>
<i class="fa fa-folder folder-icon"></i>
<span><%= it.name %></span>
</p>
</div>
<ul class="directory-subtree hidden"></ul>
</li>
</script>

</section>
</div>

+ 41
- 62
pulsar/micro/FilemanagerController.php View File

@@ -18,81 +18,60 @@ namespace Pulsar\Micro;

use Phalcon\Http\Response;
use Phalcon\DI\Injectable;
use Pulsar\Service\FilemanagerService;

class FilemanagerController extends Injectable
{
public function directoriesAction( int $recursive = 0, string $path = '/' )
/**
* Klasa główna menedżera plików.
*
* TYPE: FilemanagerService
*/
private $sfmgr = null;

/**
* Konstruktor klasy FilemanagerController.
*/
public function __construct()
{
$this->sfmgr = new FilemanagerService();
}

/**
* Akcja wywoływana podczas dostępu do listy katalogów w folderze.
*
* PARAMETERS:
* $rec: integer
* Czy program ma szukać również podfolderów?
* $path: string
* Ścieżka do folderu z którego pobierane będą katalogi.
* RETURNS: string
* Zakodowaną do formatu JSON listę folderów w podanym katalogu.
*/
public function directoriesAction( bool $rec = false, string $path = '/' )
: string
{
$path = $this->_getRealPath( $path );
$path = $this->sfmgr->getRealPath( $path );
return json_encode(
$this->_listDirectories( $path, $recursive != 0 )
$this->sfmgr->listDirectories( $path, $rec != 0 )
);
}

/**
* Akcja wywoływana podczas dostępu do listy obiektów w folderze.
*
* PARAMETERS:
* $path: string
* Ścieżka do folderu z którego pobierane będą obiekty.
* RETURNS: string
* Zakodowaną do formatu JSON listę obiektów w podanym folderze.
*/
public function entitiesAction( string $path = '/' )
: string
{
$path = $this->_getRealPath( $path );
$path = $this->sfmgr->getRealPath( $path );
return json_encode(
$this->_listEntities( $path )
$this->sfmgr->listEntities( $path, $recursive != 0 )
);
}

private function _getRealPath( string $path ): string
{
$path = realpath( BASE_PATH . 'files/' . $path );

// sprawdź czy ścieżka zawiera nazwę bazową - nie pozwól cofnąć się
// poza folder w którym użytkownik może wykonywać akcje
if( strpos( $path, BASE_PATH . 'files' ) !== 0 )
throw new \Exception("Not found");

return $path;
}

private function _listDirectories( string $path, bool $sub ): array
{
$directories = [];
$iterator = new \DirectoryIterator( $path );

// szukaj katalogów w katalogu
foreach( $iterator as $entity )
{
if( !$entity->isDir() || $entity->isDot() )
continue;

// twórz listę katalogów gdy funkcja na to zezwala
$directories[$entity->getFilename()] = $sub
? $this->_listDirectories( $entity->getRealPath(), true )
: [];
}
return $directories;
}

private function _listEntities( string $path ): array
{
$entities = [];
$iterator = new \DirectoryIterator( $path );

// szukaj elementów w katalogu
foreach( $iterator as $entity )
{
// nie uwzględniaj linków
if( $entity->isLink() || $entity->isDot() )
continue;

// uzupełnij informacje dla każdego elementu
$entities[$entity->getFilename()] = [
'size' => $entity->getSize(),
'modify' => $entity->getMTime(),
'access' => $entity->getATime(),
'type' => $entity->getType(),
'mime' => $entity->isDir()
? ''
: mime_content_type( $entity->getPathname() )
];
}
return $entities;
}
}

pulsar/services/Filemanager.php → pulsar/services/FilemanagerService.php View File

@@ -16,12 +16,9 @@

namespace Pulsar\Service;

use Phalcon\Http\Response;
use Phalcon\DI\Injectable;

abstract class Filemanager
class FilemanagerService
{
public function GetRealPath( string $path ): string
public function getRealPath( string $path ): string
{
$path = realpath( BASE_PATH . 'files/' . $path );

@@ -33,7 +30,7 @@ abstract class Filemanager
return $path;
}

public function ListDirectories( string $path, bool $sub ): array
public function listDirectories( string $path, bool $sub ): array
{
$directories = [];
$iterator = new \DirectoryIterator( $path );
@@ -45,14 +42,19 @@ abstract class Filemanager
continue;

// twórz listę katalogów gdy funkcja na to zezwala
$directories[$entity->getFilename()] = $sub
? self::ListDirectories( $entity->getRealPath(), true )
: [];
$directories[] = [
'name' => $entity->getFilename(),
'modify' => $entity->getMTime(),
'access' => $entity->getATime(),
'children' => $sub
? $this->listDirectories( $entity->getRealPath(), true )
: []
];
}
return $directories;
}

public function ListEntities( string $path ): array
public function listEntities( string $path ): array
{
$entities = [];
$iterator = new \DirectoryIterator( $path );

+ 8
- 1
tsconfig.static.json View File

@@ -1,7 +1,7 @@
{
"compilerOptions": {
"module": "None",
"target": "ES5",
"target": "es2015",
"noImplicitAny": true,
"removeComments": true,
"preserveConstEnums": true,
@@ -10,8 +10,15 @@
"outFile": "public/themes/pluto/js/static.js"
},
"files": [
"public/themes/pluto/src.static/interfaces/IEntity.ts",
"public/themes/pluto/src.static/interfaces/IObservable.ts",
"public/themes/pluto/src.static/interfaces/IRender.ts",
"public/themes/pluto/src.static/extra/ObservableArray.ts",
"public/themes/pluto/src.static/extra/RenderArray.ts",
"public/themes/pluto/src.static/extra/RenderArrayTree.ts",
"public/themes/pluto/src.static/controls/CheckBox.ts",
"public/themes/pluto/src.static/controls/TabControl.ts",
"public/themes/pluto/src.static/controls/FileManager.ts",
"public/themes/pluto/src.static/Application.ts",
"public/themes/pluto/src.static/Prototypes.ts"
]

Loading…
Cancel
Save