Toggle navigation
Toggle navigation
This project
Loading...
Sign in
Зуев Егор
/
wiki.dev
Go to a project
Toggle navigation
Toggle navigation pinning
Projects
Groups
Snippets
Help
Project
Activity
Repository
Pipelines
Graphs
Issues
0
Merge Requests
0
Wiki
Snippets
Network
Create a new issue
Commits
Issue Boards
Files
Commits
Network
Compare
Branches
Tags
Authored by
Dan Brown
2016-03-12 15:52:19 +0000
Browse Files
Options
Browse Files
Tag
Download
Email Patches
Plain Diff
Commit
30214fde74c954ee6cf4daeb562764343b546b58
30214fde
1 parent
93ebdf72
Added UI components of page autosaving
Hide whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
237 additions
and
35 deletions
app/Http/Controllers/PageController.php
app/Http/routes.php
app/Repos/PageRepo.php
resources/assets/js/controllers.js
resources/assets/js/directives.js
resources/assets/js/global.js
resources/assets/js/pages/page-form.js
resources/assets/sass/_header.scss
resources/assets/sass/_variables.scss
resources/assets/sass/styles.scss
resources/views/pages/form.blade.php
resources/views/partials/notifications.blade.php
app/Http/Controllers/PageController.php
View file @
30214fd
...
...
@@ -108,6 +108,17 @@ class PageController extends Controller
}
/**
* Get page from an ajax request.
* @param $pageId
* @return \Illuminate\Http\JsonResponse
*/
public
function
getPageAjax
(
$pageId
)
{
$page
=
$this
->
pageRepo
->
getById
(
$pageId
);
return
response
()
->
json
(
$page
);
}
/**
* Show the form for editing the specified page.
* @param $bookSlug
* @param $pageSlug
...
...
@@ -119,6 +130,24 @@ class PageController extends Controller
$page
=
$this
->
pageRepo
->
getBySlug
(
$pageSlug
,
$book
->
id
);
$this
->
checkOwnablePermission
(
'page-update'
,
$page
);
$this
->
setPageTitle
(
'Editing Page '
.
$page
->
getShortName
());
$page
->
isDraft
=
false
;
// Check for active editing and drafts
$warnings
=
[];
if
(
$this
->
pageRepo
->
isPageEditingActive
(
$page
,
60
))
{
$warnings
[]
=
$this
->
pageRepo
->
getPageEditingActiveMessage
(
$page
,
60
);
}
if
(
$this
->
pageRepo
->
hasUserGotPageDraft
(
$page
,
$this
->
currentUser
->
id
))
{
$draft
=
$this
->
pageRepo
->
getUserPageDraft
(
$page
,
$this
->
currentUser
->
id
);
$page
->
name
=
$draft
->
name
;
$page
->
html
=
$draft
->
html
;
$page
->
isDraft
=
true
;
$warnings
[]
=
$this
->
pageRepo
->
getUserPageDraftMessage
(
$draft
);
}
if
(
count
(
$warnings
)
>
0
)
session
()
->
flash
(
'warning'
,
implode
(
"
\n
"
,
$warnings
));
return
view
(
'pages/edit'
,
[
'page'
=>
$page
,
'book'
=>
$book
,
'current'
=>
$page
]);
}
...
...
@@ -155,8 +184,9 @@ class PageController extends Controller
]);
$page
=
$this
->
pageRepo
->
getById
(
$pageId
);
$this
->
checkOwnablePermission
(
'page-update'
,
$page
);
$this
->
pageRepo
->
saveUpdateDraft
(
$page
,
$request
->
only
([
'name'
,
'html'
]));
return
response
()
->
json
([
'status'
=>
'success'
,
'message'
=>
'Draft successfully saved'
]);
$draft
=
$this
->
pageRepo
->
saveUpdateDraft
(
$page
,
$request
->
only
([
'name'
,
'html'
]));
$updateTime
=
$draft
->
updated_at
->
format
(
'H:i'
);
return
response
()
->
json
([
'status'
=>
'success'
,
'message'
=>
'Draft saved at '
.
$updateTime
]);
}
/**
...
...
app/Http/routes.php
View file @
30214fd
...
...
@@ -77,6 +77,7 @@ Route::group(['middleware' => 'auth'], function () {
// Ajax routes
Route
::
put
(
'/ajax/page/{id}/save-draft'
,
'PageController@saveUpdateDraft'
);
Route
::
get
(
'/ajax/page/{id}'
,
'PageController@getPageAjax'
);
// Links
Route
::
get
(
'/link/{id}'
,
'PageController@redirectFromLink'
);
...
...
app/Repos/PageRepo.php
View file @
30214fd
...
...
@@ -4,6 +4,7 @@
use
Activity
;
use
BookStack\Book
;
use
BookStack\Exceptions\NotFoundException
;
use
Carbon\Carbon
;
use
DOMDocument
;
use
Illuminate\Support\Str
;
use
BookStack\Page
;
...
...
@@ -259,11 +260,16 @@ class PageRepo extends EntityRepo
}
// Update with new details
$userId
=
auth
()
->
user
()
->
id
;
$page
->
fill
(
$input
);
$page
->
html
=
$this
->
formatHtml
(
$input
[
'html'
]);
$page
->
text
=
strip_tags
(
$page
->
html
);
$page
->
updated_by
=
auth
()
->
user
()
->
i
d
;
$page
->
updated_by
=
$userI
d
;
$page
->
save
();
// Remove all update drafts for this user & page.
$this
->
userUpdateDraftsQuery
(
$page
,
$userId
)
->
delete
();
return
$page
;
}
...
...
@@ -318,10 +324,7 @@ class PageRepo extends EntityRepo
public
function
saveUpdateDraft
(
Page
$page
,
$data
=
[])
{
$userId
=
auth
()
->
user
()
->
id
;
$drafts
=
$this
->
pageRevision
->
where
(
'created_by'
,
'='
,
$userId
)
->
where
(
'type'
,
'update_draft'
)
->
where
(
'page_id'
,
'='
,
$page
->
id
)
->
orderBy
(
'created_at'
,
'desc'
)
->
get
();
$drafts
=
$this
->
userUpdateDraftsQuery
(
$page
,
$userId
)
->
get
();
if
(
$drafts
->
count
()
>
0
)
{
$draft
=
$drafts
->
first
();
...
...
@@ -340,6 +343,107 @@ class PageRepo extends EntityRepo
}
/**
* The base query for getting user update drafts.
* @param Page $page
* @param $userId
* @return mixed
*/
private
function
userUpdateDraftsQuery
(
Page
$page
,
$userId
)
{
return
$this
->
pageRevision
->
where
(
'created_by'
,
'='
,
$userId
)
->
where
(
'type'
,
'update_draft'
)
->
where
(
'page_id'
,
'='
,
$page
->
id
)
->
orderBy
(
'created_at'
,
'desc'
);
}
/**
* Checks whether a user has a draft version of a particular page or not.
* @param Page $page
* @param $userId
* @return bool
*/
public
function
hasUserGotPageDraft
(
Page
$page
,
$userId
)
{
return
$this
->
userUpdateDraftsQuery
(
$page
,
$userId
)
->
count
()
>
0
;
}
/**
* Get the latest updated draft revision for a particular page and user.
* @param Page $page
* @param $userId
* @return mixed
*/
public
function
getUserPageDraft
(
Page
$page
,
$userId
)
{
return
$this
->
userUpdateDraftsQuery
(
$page
,
$userId
)
->
first
();
}
/**
* Get the notification message that informs the user that they are editing a draft page.
* @param PageRevision $draft
* @return string
*/
public
function
getUserPageDraftMessage
(
PageRevision
$draft
)
{
$message
=
'You are currently editing a draft that was last saved '
.
$draft
->
updated_at
->
diffForHumans
()
.
'.'
;
if
(
$draft
->
page
->
updated_at
->
timestamp
>
$draft
->
updated_at
->
timestamp
)
{
$message
.=
"
\n
This page has been updated by since that time. It is recommended that you discard this draft."
;
}
return
$message
;
}
/**
* Check if a page is being actively editing.
* Checks for edits since last page updated.
* Passing in a minuted range will check for edits
* within the last x minutes.
* @param Page $page
* @param null $minRange
* @return bool
*/
public
function
isPageEditingActive
(
Page
$page
,
$minRange
=
null
)
{
$draftSearch
=
$this
->
activePageEditingQuery
(
$page
,
$minRange
);
return
$draftSearch
->
count
()
>
0
;
}
/**
* Get a notification message concerning the editing activity on
* a particular page.
* @param Page $page
* @param null $minRange
* @return string
*/
public
function
getPageEditingActiveMessage
(
Page
$page
,
$minRange
=
null
)
{
$pageDraftEdits
=
$this
->
activePageEditingQuery
(
$page
,
$minRange
)
->
get
();
$userMessage
=
$pageDraftEdits
->
count
()
>
1
?
$pageDraftEdits
->
count
()
.
' users have'
:
$pageDraftEdits
->
first
()
->
createdBy
->
name
.
' has'
;
$timeMessage
=
$minRange
===
null
?
'since the page was last updated'
:
'in the last '
.
$minRange
.
' minutes'
;
$message
=
'%s started editing this page %s. Take care not to overwrite each other\'s updates!'
;
return
sprintf
(
$message
,
$userMessage
,
$timeMessage
);
}
/**
* A query to check for active update drafts on a particular page.
* @param Page $page
* @param null $minRange
* @return mixed
*/
private
function
activePageEditingQuery
(
Page
$page
,
$minRange
=
null
)
{
$query
=
$this
->
pageRevision
->
where
(
'type'
,
'='
,
'update_draft'
)
->
where
(
'updated_at'
,
'>'
,
$page
->
updated_at
)
->
where
(
'created_by'
,
'!='
,
auth
()
->
user
()
->
id
)
->
with
(
'createdBy'
);
if
(
$minRange
!==
null
)
{
$query
=
$query
->
where
(
'updated_at'
,
'>='
,
Carbon
::
now
()
->
subMinutes
(
$minRange
));
}
return
$query
;
}
/**
* Gets a single revision via it's id.
* @param $id
* @return mixed
...
...
resources/assets/js/controllers.js
View file @
30214fd
...
...
@@ -213,49 +213,85 @@ module.exports = function (ngApp, events) {
}]);
ngApp
.
controller
(
'PageEditController'
,
[
'$scope'
,
'$http'
,
'$attrs'
,
'$interval'
,
function
(
$scope
,
$http
,
$attrs
,
$interval
)
{
ngApp
.
controller
(
'PageEditController'
,
[
'$scope'
,
'$http'
,
'$attrs'
,
'$interval'
,
'$timeout'
,
function
(
$scope
,
$http
,
$attrs
,
$interval
,
$timeout
)
{
$scope
.
editorOptions
=
require
(
'./pages/page-form'
);
$scope
.
editorHtml
=
''
;
$scope
.
draftText
=
''
;
var
pageId
=
Number
(
$attrs
.
pageId
);
var
isEdit
=
pageId
!==
0
;
var
autosaveFrequency
=
30
;
// AutoSave interval in seconds.
$scope
.
isDraft
=
Number
(
$attrs
.
pageDraft
)
===
1
;
if
(
$scope
.
isDraft
)
$scope
.
draftText
=
'Editing Draft'
;
var
autoSave
=
false
;
var
currentContent
=
{
title
:
false
,
html
:
false
};
if
(
isEdit
)
{
startAutoSave
();
setTimeout
(()
=>
{
startAutoSave
();
},
1000
);
}
$scope
.
editorChange
=
function
()
{
$scope
.
draftText
=
''
;
}
$scope
.
editorChange
=
function
()
{}
/**
* Start the AutoSave loop, Checks for content change
* before performing the costly AJAX request.
*/
function
startAutoSave
()
{
var
currentTitle
=
$
(
'#name'
).
val
();
var
currentHtml
=
$scope
.
editorHtml
;
console
.
log
(
'Starting auto save'
);
currentContent
.
title
=
$
(
'#name'
).
val
();
currentContent
.
html
=
$scope
.
editorHtml
;
$interval
(()
=>
{
autoSave
=
$interval
(()
=>
{
var
newTitle
=
$
(
'#name'
).
val
();
var
newHtml
=
$scope
.
editorHtml
;
if
(
newTitle
!==
current
Title
||
newHtml
!==
currentH
tml
)
{
current
H
tml
=
newHtml
;
current
T
itle
=
newTitle
;
if
(
newTitle
!==
current
Content
.
title
||
newHtml
!==
currentContent
.
h
tml
)
{
current
Content
.
h
tml
=
newHtml
;
current
Content
.
t
itle
=
newTitle
;
saveDraftUpdate
(
newTitle
,
newHtml
);
}
},
1000
*
5
);
},
1000
*
autosaveFrequency
);
}
/**
* Save a draft update into the system via an AJAX request.
* @param title
* @param html
*/
function
saveDraftUpdate
(
title
,
html
)
{
$http
.
put
(
'/ajax/page/'
+
pageId
+
'/save-draft'
,
{
name
:
title
,
html
:
html
}).
then
((
responseData
)
=>
{
$scope
.
draftText
=
'Draft saved'
})
$scope
.
draftText
=
responseData
.
data
.
message
;
$scope
.
isDraft
=
true
;
});
}
/**
* Discard the current draft and grab the current page
* content from the system via an AJAX request.
*/
$scope
.
discardDraft
=
function
()
{
$http
.
get
(
'/ajax/page/'
+
pageId
).
then
((
responseData
)
=>
{
if
(
autoSave
)
$interval
.
cancel
(
autoSave
);
$scope
.
draftText
=
''
;
$scope
.
isDraft
=
false
;
$scope
.
$broadcast
(
'html-update'
,
responseData
.
data
.
html
);
$
(
'#name'
).
val
(
currentContent
.
title
);
$timeout
(()
=>
{
startAutoSave
();
},
1000
);
events
.
emit
(
'success'
,
'Draft discarded, The editor has been updated with the current page content'
);
});
};
}]);
};
\ No newline at end of file
...
...
resources/assets/js/directives.js
View file @
30214fd
...
...
@@ -162,7 +162,7 @@ module.exports = function (ngApp, events) {
};
}]);
ngApp
.
directive
(
'tinymce'
,
[
function
(
)
{
ngApp
.
directive
(
'tinymce'
,
[
'$timeout'
,
function
(
$timeout
)
{
return
{
restrict
:
'A'
,
scope
:
{
...
...
@@ -173,14 +173,24 @@ module.exports = function (ngApp, events) {
link
:
function
(
scope
,
element
,
attrs
)
{
function
tinyMceSetup
(
editor
)
{
editor
.
on
(
'
keyup
'
,
(
e
)
=>
{
editor
.
on
(
'
ExecCommand change NodeChange ObjectResized
'
,
(
e
)
=>
{
var
content
=
editor
.
getContent
();
console
.
log
(
content
);
scope
.
$apply
(()
=>
{
$timeout
(()
=>
{
scope
.
mceModel
=
content
;
});
scope
.
mceChange
(
content
);
});
editor
.
on
(
'init'
,
(
e
)
=>
{
scope
.
mceModel
=
editor
.
getContent
();
});
scope
.
$on
(
'html-update'
,
(
event
,
value
)
=>
{
editor
.
setContent
(
value
);
editor
.
selection
.
select
(
editor
.
getBody
(),
true
);
editor
.
selection
.
collapse
(
false
);
scope
.
mceModel
=
editor
.
getContent
();
});
}
scope
.
tinymce
.
extraSetups
.
push
(
tinyMceSetup
);
...
...
resources/assets/js/global.js
View file @
30214fd
...
...
@@ -54,10 +54,10 @@ $.expr[":"].contains = $.expr.createPseudo(function (arg) {
// Global jQuery Elements
$
(
function
()
{
var
notifications
=
$
(
'.notification'
);
var
successNotification
=
notifications
.
filter
(
'.pos'
);
var
errorNotification
=
notifications
.
filter
(
'.neg'
);
var
warningNotification
=
notifications
.
filter
(
'.warning'
);
// Notification Events
window
.
Events
.
listen
(
'success'
,
function
(
text
)
{
successNotification
.
hide
();
...
...
@@ -66,6 +66,10 @@ $(function () {
successNotification
.
show
();
},
1
);
});
window
.
Events
.
listen
(
'warning'
,
function
(
text
)
{
warningNotification
.
find
(
'span'
).
text
(
text
);
warningNotification
.
show
();
});
window
.
Events
.
listen
(
'error'
,
function
(
text
)
{
errorNotification
.
find
(
'span'
).
text
(
text
);
errorNotification
.
show
();
...
...
resources/assets/js/pages/page-form.js
View file @
30214fd
...
...
@@ -54,8 +54,6 @@ var mceOptions = module.exports = {
extraSetups
:
[],
setup
:
function
(
editor
)
{
console
.
log
(
mceOptions
.
extraSetups
);
for
(
var
i
=
0
;
i
<
mceOptions
.
extraSetups
.
length
;
i
++
)
{
mceOptions
.
extraSetups
[
i
](
editor
);
}
...
...
resources/assets/sass/_header.scss
View file @
30214fd
...
...
@@ -161,6 +161,12 @@ form.search-box {
}
}
.faded
>
span
.faded-text
{
display
:
inline-block
;
padding
:
$-s
;
opacity
:
0
.5
;
}
.faded-small
{
color
:
#000
;
font-size
:
0
.9em
;
...
...
resources/assets/sass/_variables.scss
View file @
30214fd
...
...
@@ -38,6 +38,7 @@ $primary-dark: #0288D1;
$secondary
:
#e27b41
;
$positive
:
#52A256
;
$negative
:
#E84F4F
;
$warning
:
$secondary
;
$primary-faded
:
rgba
(
21
,
101
,
192
,
0
.15
);
// Item Colors
...
...
resources/assets/sass/styles.scss
View file @
30214fd
...
...
@@ -88,6 +88,10 @@ body.dragging, body.dragging * {
background-color
:
$negative
;
color
:
#EEE
;
}
&
.warning
{
background-color
:
$secondary
;
color
:
#EEE
;
}
}
// Loading icon
...
...
resources/views/pages/form.blade.php
View file @
30214fd
<div
class=
"page-editor flex-fill flex"
ng-controller=
"PageEditController"
page-id=
"{{ $model->id or 0 }}"
>
<div
class=
"page-editor flex-fill flex"
ng-controller=
"PageEditController"
page-id=
"{{ $model->id or 0 }}"
page-draft=
"{{ $page->isDraft or 0 }}"
>
{{ csrf_field() }}
<div
class=
"faded-small toolbar"
>
...
...
@@ -9,15 +9,19 @@
<div
class=
"row"
>
<div
class=
"col-sm-4 faded"
>
<div
class=
"action-buttons text-left"
>
<a
href=
"{{ back()->getTargetUrl() }}"
class=
"text-button text-primary"
><i
class=
"zmdi zmdi-arrow-left"
></i>
Back
</a>
<a
onclick=
"$('body>header').slideToggle();"
class=
"text-button text-primary"
><i
class=
"zmdi zmdi-swap-vertical"
></i>
Toggle Header
</a>
</div>
</div>
<div
class=
"col-sm-4 faded text-center"
>
<span
ng-bind=
"draftText"
></span>
<div
class=
"action-buttons text-center"
ng-cloak
>
<span
class=
"faded-text"
ng-bind=
"draftText"
></span>
<button
type=
"button"
ng-if=
"isDraft"
ng-click=
"discardDraft()"
class=
"text-button text-neg"
><i
class=
"zmdi zmdi-close-circle"
></i>
Discard Draft
</button>
</div>
</div>
<div
class=
"col-sm-4 faded"
>
<div
class=
"action-buttons"
>
<a
href=
"{{ back()->getTargetUrl() }}"
class=
"text-button text-primary"
><i
class=
"zmdi zmdi-close"
></i>
Cancel
</a>
<button
type=
"submit"
id=
"save-button"
class=
"text-button text-pos"
><i
class=
"zmdi zmdi-floppy"
></i>
Save Page
</button>
</div>
</div>
...
...
resources/views/partials/notifications.blade.php
View file @
30214fd
<div
class=
"notification anim pos"
@
if
(!
Session::has
('
success
'))
style=
"display:none;"
@
endif
>
<i
class=
"zmdi zmdi-check-circle"
></i>
<span>
{{ Session::get('success') }}
</span>
<i
class=
"zmdi zmdi-check-circle"
></i>
<span>
{!! nl2br(htmlentities(Session::get('success'))) !!}
</span>
</div>
<div
class=
"notification anim warning stopped"
@
if
(!
Session::has
('
warning
'))
style=
"display:none;"
@
endif
>
<i
class=
"zmdi zmdi-info"
></i>
<span>
{!! nl2br(htmlentities(Session::get('warning'))) !!}
</span>
</div>
<div
class=
"notification anim neg stopped"
@
if
(!
Session::has
('
error
'))
style=
"display:none;"
@
endif
>
<i
class=
"zmdi zmdi-alert-circle"
></i>
<span>
{
{ Session::get('error') }
}
</span>
<i
class=
"zmdi zmdi-alert-circle"
></i>
<span>
{
!! nl2br(htmlentities(Session::get('error'))) !!
}
</span>
</div>
...
...
Please
register
or
sign in
to post a comment