1
0
Fork 0
mirror of https://github.com/documize/community.git synced 2025-07-20 13:49:42 +02:00

saving and inserting reusable content blocks

This commit is contained in:
Harvey Kandola 2017-01-20 14:24:38 -08:00
parent fdbf03b25c
commit b7fa3b9006
32 changed files with 1334 additions and 768 deletions

View file

@ -15,6 +15,7 @@ import NotifierMixin from '../../mixins/notifier';
export default Ember.Component.extend(TooltipMixin, NotifierMixin, {
documentService: Ember.inject.service('document'),
sectionService: Ember.inject.service('section'),
document: {},
folder: {},
showToc: true,
@ -22,6 +23,13 @@ export default Ember.Component.extend(TooltipMixin, NotifierMixin, {
showScrollTool: false,
showingSections: false,
init() {
this._super(...arguments);
this.get('sectionService').getSpaceSectionTemplates(this.get('folder.id')).then((t) => {
this.set('templates', t);
});
},
didRender() {
if (this.session.authenticated) {
this.addTooltip(document.getElementById("section-tool"));
@ -93,6 +101,11 @@ export default Ember.Component.extend(TooltipMixin, NotifierMixin, {
this.attrs.onAddSection(section);
},
onInsertTemplate(template) {
this.send('showToc');
this.attrs.onInsertTemplate(template);
},
scrollTop() {
this.set('showScrollTool', false);

View file

@ -17,6 +17,7 @@ export default Ember.Component.extend(NotifierMixin, TooltipMixin, {
appMeta: Ember.inject.service(),
userService: Ember.inject.service('user'),
localStorage: Ember.inject.service(),
pinned: Ember.inject.service(),
drop: null,
users: [],
menuOpen: false,
@ -24,7 +25,6 @@ export default Ember.Component.extend(NotifierMixin, TooltipMixin, {
name: "",
description: ""
},
pinned: Ember.inject.service(),
pinState : {
isPinned: false,
pinId: '',

View file

@ -70,6 +70,16 @@ export default Ember.Component.extend(NotifierMixin, TooltipMixin, {
},
actions: {
onSaveAsPage(id, title) {
let params = {
documentId: this.get('document.id'),
pageId: id,
title: title,
};
this.attrs.onSaveAsPage(params);
},
onDeletePage(id, deleteChildren) {
let page = this.get('pages').findBy("id", id);

View file

@ -18,24 +18,44 @@ const {
export default Ember.Component.extend(TooltipMixin, {
deleteChildren: false,
menuOpen: false,
saveAsTitle: "",
checkId: computed('page', function () {
let id = this.get('page.id');
return `delete-check-button-${id}`;
}),
dropTarget: computed('page', function () {
menuTarget: computed('page', function () {
let id = this.get('page.id');
return `page-menu-${id}`;
}),
deleteButtonId: computed('page', function () {
let id = this.get('page.id');
return `delete-page-button-${id}`;
}),
saveAsTarget: computed('page', function () {
let id = this.get('page.id');
return `saveas-page-button-${id}`;
}),
saveAsDialogId: computed('page', function () {
let id = this.get('page.id');
return `save-as-dialog-${id}`;
}),
saveAsTitleId: computed('page', function () {
let id = this.get('page.id');
return `save-as-title-${id}`;
}),
didRender() {
if (this.get('isEditor')) {
let self = this;
$(".page-edit-button, .page-delete-button").each(function (i, el) {
$(".page-action-button").each(function (i, el) {
self.addTooltip(el);
});
}
$("#" + this.get('saveAsTitleId')).removeClass('error');
},
willDestroyElement() {
@ -43,6 +63,14 @@ export default Ember.Component.extend(TooltipMixin, {
},
actions: {
onMenuOpen() {
if ($('#' + this.get('saveAsDialogId')).is( ":visible" )) {
return;
}
this.set('menuOpen', !this.get('menuOpen'));
},
editPage(id) {
this.attrs.onEditPage(id);
},
@ -50,5 +78,21 @@ export default Ember.Component.extend(TooltipMixin, {
deletePage(id) {
this.attrs.onDeletePage(id, this.get('deleteChildren'));
},
saveAsPage(id) {
let titleElem = '#' + this.get('saveAsTitleId');
let saveAsTitle = this.get('saveAsTitle');
if (is.empty(saveAsTitle)) {
$(titleElem).addClass('error');
return;
}
this.attrs.onSaveAsPage(id, saveAsTitle);
this.set('menuOpen', false);
this.set('saveAsTitle', '');
$(titleElem).removeClass('error');
return true;
},
}
});

View file

@ -14,6 +14,12 @@ import NotifierMixin from '../../mixins/notifier';
export default Ember.Component.extend(NotifierMixin, {
display: 'section', // which CSS to use
hasTemplates: false,
didReceiveAttrs() {
console.log(this.get('templates.length'));
this.set('hasTemplates', this.get('templates.length') > 0);
},
didRender() {
let self = this;
@ -34,7 +40,11 @@ export default Ember.Component.extend(NotifierMixin, {
},
addSection(section) {
this.attrs.onAction(section);
this.attrs.onAddSection(section);
},
insertTemplate(template) {
this.attrs.onInsertTemplate(template);
}
}
});

View file

@ -57,16 +57,12 @@ export default Ember.Component.extend({
classes: 'drop-theme-basic',
position: self.get('position'),
openOn: self.get('open'),
constrainToWindow: false,
constrainToScrollParent: false,
tetherOptions: {
offset: self.offset,
targetOffset: self.targetOffset,
// optimizations: {
// moveElement: false
// },
constraints: [{
to: 'window',
attachment: 'together'
}]
targetModifier: 'scroll-handle'
},
remove: true
});

View file

@ -24,10 +24,6 @@ export default Ember.Component.extend({
didReceiveAttrs() {
this.set("contentId", 'dropdown-menu-' + stringUtil.makeId(10));
// if (this.session.get('isMobile')) {
// this.set('open', "click");
// }
},
didInsertElement() {
@ -40,10 +36,14 @@ export default Ember.Component.extend({
classes: 'drop-theme-menu',
position: self.get('position'),
openOn: self.get('open'),
constrainToWindow: false,
constrainToScrollParent: false,
tetherOptions: {
offset: "5px 0",
targetOffset: "10px 0"
}
targetOffset: "10px 0",
targetModifier: 'scroll-handle',
},
remove: true
});
if (drop) {

View file

@ -0,0 +1,28 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
//
// You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>.
//
// https://documize.com
import Model from 'ember-data/model';
import attr from 'ember-data/attr';
export default Model.extend({
documentId: attr('string'),
orgId: attr('string'),
contentType: attr('string'),
pageType: attr('string'),
preset: attr('boolean', { defaultValue: false }),
presetId: attr('string'),
title: attr('string'),
body: attr('string'),
firstname: attr('string'),
lastname: attr('string'),
created: attr(),
revised: attr()
});

View file

@ -22,6 +22,8 @@ export default Model.extend({
level: attr('number', { defaultValue: 1 }),
sequence: attr('number', { defaultValue: 0 }),
revisions: attr('number', { defaultValue: 0 }),
preset: attr('boolean', { defaultValue: false }),
presetId: attr('string'),
title: attr('string'),
body: attr('string'),
rawBody: attr('string'),

View file

@ -142,6 +142,51 @@ export default Ember.Controller.extend(NotifierMixin, {
});
},
onInsertTemplate(template) {
this.audit.record("added-section-template-" + template.get('contentType'));
let page = {
documentId: this.get('model.document.id'),
title: `${template.get('title')}`,
level: 1,
sequence: 0,
body: template.get('body'),
contentType: template.get('contentType'),
pageType: template.get('pageType'),
presetId: template.get('id')
};
let meta = {
documentId: this.get('model.document.id'),
rawBody: "",
config: ""
};
let model = {
page: page,
meta: meta
};
this.get('documentService').addPage(this.get('model.document.id'), model).then((newPage) => {
let data = this.get('store').normalize('page', newPage);
this.get('store').push(data);
this.get('documentService').getPages(this.get('model.document.id')).then((pages) => {
this.set('model.pages', pages.filterBy('pageType', 'section'));
this.set('model.tabs', pages.filterBy('pageType', 'tab'));
this.get('documentService').getPageMeta(this.get('model.document.id'), newPage.id).then(() => {
this.transitionToRoute('document.edit',
this.get('model.folder.id'),
this.get('model.folder.slug'),
this.get('model.document.id'),
this.get('model.document.slug'),
newPage.id);
});
});
});
},
onDocumentDelete() {
this.get('documentService').deleteDocument(this.get('model.document.id')).then(() => {
this.audit.record("deleted-page");

View file

@ -14,6 +14,7 @@ import NotifierMixin from '../../../mixins/notifier';
export default Ember.Controller.extend(NotifierMixin, {
documentService: Ember.inject.service('document'),
sectionService: Ember.inject.service('section'),
queryParams: ['page'],
// Jump to the right part of the document.
@ -85,6 +86,12 @@ export default Ember.Controller.extend(NotifierMixin, {
});
},
onSaveAsPage(params) {
this.get('sectionService').saveSectionTemplate(params).then(() => {
this.showNotification("Published");
});
},
onPageDeleted(deletePage) {
let documentId = this.get('model.document.id');
let pages = this.get('model.pages');

View file

@ -1,2 +1,2 @@
{{document/document-view document=model.document links=model.links allPages=model.allPages tabs=model.tabs pages=model.pages folder=model.folder folders=model.folders isEditor=model.isEditor
gotoPage=(action 'gotoPage') onDeletePage=(action 'onPageDeleted')}}
gotoPage=(action 'gotoPage') onSaveAsPage=(action 'onSaveAsPage') onDeletePage=(action 'onPageDeleted')}}

View file

@ -2,7 +2,7 @@
{{#layout/zone-sidebar}}
{{document/document-sidebar document=model.document folder=model.folder pages=model.pages page=model.page isEditor=model.isEditor sections=model.sections
onAddSection=(action 'onAddSection') changePageSequence=(action 'onPageSequenceChange') changePageLevel=(action 'onPageLevelChange') gotoPage=(action 'gotoPage')}}
onAddSection=(action 'onAddSection') onInsertTemplate=(action 'onInsertTemplate') changePageSequence=(action 'onPageSequenceChange') changePageLevel=(action 'onPageLevelChange') gotoPage=(action 'gotoPage')}}
{{/layout/zone-sidebar}}
{{#layout/zone-content}}

View file

@ -296,7 +296,7 @@ export default Ember.Service.extend({
return this.get('ajax').request(`documents/${documentId}/attachments/${attachmentId}`, {
method: 'DELETE'
});
},
}
});
function isObject(a) {

View file

@ -68,5 +68,35 @@ export default BaseService.extend({
return pages;
});
},
/******************************
* Reusable section blocks
******************************/
// Saves section as template
saveSectionTemplate(payload) {
let url = `sections/templates`;
return this.get('ajax').post(url, {
data: JSON.stringify(payload),
contentType: 'json'
});
},
// Returns all available sections.
getSpaceSectionTemplates(folderId) {
return this.get('ajax').request(`sections/templates/${folderId}`, {
method: 'GET'
}).then((response) => {
let data = [];
data = response.map((obj) => {
let data = this.get('store').normalize('pageTemplate', obj);
return this.get('store').push(data);
});
return data;
});
}
});

View file

@ -41,7 +41,7 @@
> .is-a-page {
.page-title {
> .page-toolbar {
opacity: 0.3;
opacity: 0.5;
@extend .transition-all;
&:hover {

View file

@ -4,6 +4,17 @@
> .canvas {
padding: 0;
> .divider {
margin: 30px 0 20px 0;
border-top: 1px dotted $color-gray;
}
> .template-caption {
text-align: center;
color: $color-gray;
padding-bottom: 10px;
}
> .list {
margin: 0;
padding: 0;

View file

@ -97,6 +97,15 @@
}
}
> li.danger {
color: $color-red;
&:hover {
color: $color-red;
font-weight: bold;
}
}
> li.divider {
height: 1px;
border-top: 1px solid $color-border;

View file

@ -25,7 +25,7 @@
gotoPage=(action 'gotoPage')}}
{{/if}}
{{#if showSections}}
{{document/page-wizard display='section' document=document folder=folder sections=sections
onCancel=(action 'onCancel') onAction=(action 'onAddSection')}}
{{document/page-wizard display='section' document=document folder=folder sections=sections templates=templates
onCancel=(action 'onCancel') onAddSection=(action 'onAddSection') onInsertTemplate=(action 'onInsertTemplate')}}
{{/if}}
</div>

View file

@ -59,7 +59,7 @@
{{#if isEditor}}
<li class="divider"></li>
<li class="item" id="delete-document-button">Delete</li>
<li class="item danger" id="delete-document-button">Delete</li>
{{/if}}
</ul>
{{/dropdown-menu}}
@ -74,20 +74,17 @@
{{#if session.authenticated}}
{{#unless pinState.isPinned}}
{{#dropdown-dialog target="pin-document-button" position="bottom right" button="Pin" color="flat-green" onAction=(action 'pin') focusOn="pin-document-name" }}
<div>
<div class="input-control">
<label>Pin Document</label>
<div class="tip">A 3 or 4 character name</div>
{{input type='text' id="pin-document-name" value=pinState.newName}}
</div>
</div>
{{/dropdown-dialog}}
{{/unless}}
{{/if}}
{{#if isEditor}}
{{#dropdown-dialog target="save-template-button" position="bottom right" button="Save as Template" color="flat-green" onAction=(action 'saveTemplate') focusOn="new-template-name" }}
<div>
<div class="input-control">
<label>Name</label>
<div class="tip">Short name for this type of document</div>
@ -98,7 +95,6 @@
<div class="tip">Explain use case for this template</div>
{{textarea value=saveTemplate.description rows="3" id="new-template-desc"}}
</div>
</div>
{{/dropdown-dialog}}
{{#dropdown-dialog target="set-meta-button" position="bottom right" button="Save" color="flat-green" onAction=(action 'saveMeta') focusOn="meta-name" }}

View file

@ -17,7 +17,7 @@
{{#each pages key="id" as |page index|}}
<div class="wysiwyg">
<div id="page-{{ page.id }}" class="is-a-page" data-id="{{ page.id }}" data-type="{{ page.contentType }}">
{{document/page-heading tagName=page.tagName document=document folder=folder page=page isEditor=isEditor onDeletePage=(action 'onDeletePage')}}
{{document/page-heading tagName=page.tagName document=document folder=folder page=page isEditor=isEditor onSaveAsPage=(action 'onSaveAsPage') onDeletePage=(action 'onDeletePage')}}
{{section/base-renderer page=page}}
</div>
</div>

View file

@ -3,20 +3,46 @@
<div id="page-toolbar-{{ page.id }}" class="pull-right page-toolbar hidden-xs hidden-sm">
{{#if isEditor}}
{{#link-to 'document.edit' folder.id folder.slug document.id document.slug page.id}}
<div class="round-button-mono page-edit-button" data-tooltip="Edit" data-tooltip-position="top center">
<i class="material-icons">mode_edit</i>
<div class="round-button-mono page-action-button" data-tooltip="Edit" data-tooltip-position="top center">
<i class="material-icons color-gray">mode_edit</i>
</div>
{{/link-to}}
<div id="delete-page-button-{{page.id}}" class="round-button-mono page-delete-button" data-tooltip="Delete" data-tooltip-position="top center">
<i class="material-icons">delete</i>
<div id="page-menu-{{page.id}}" class="round-button-mono page-action-button" data-tooltip="More options" data-tooltip-position="top center">
<i class="material-icons color-gray">more_vert</i>
</div>
{{#dropdown-dialog target=dropTarget position="top right" button="Delete" color="flat-red" onAction=(action 'deletePage' page.id)}}
{{#dropdown-menu target=menuTarget position="bottom right" open="click" onOpenCallback=(action 'onMenuOpen') onCloseCallback=(action 'onMenuOpen')}}
<ul class="menu">
<li class="item">Duplicate</li>
<li class="item">Move</li>
<li class="item" id="saveas-page-button-{{page.id}}">Publish</li>
<li class="divider"></li>
<li class="item danger" id={{deleteButtonId}}>Delete</li>
</ul>
{{/dropdown-menu}}
{{#if menuOpen}}
{{#dropdown-dialog target=deleteButtonId position="top right" button="Delete" color="flat-red" onAction=(action 'deletePage' page.id)}}
<p>Are you sure you want to delete <span class="bold">{{page.title}}?</span></p>
<p>
{{input type="checkbox" id=checkId class="margin-left-20" checked=deleteChildren}}
<label for="{{checkId}}">&nbsp;Delete child pages</label>
</p>
{{/dropdown-dialog}}
{{#dropdown-dialog id=saveAsDialogId target=saveAsTarget position="top right" button="Publish" color="flat-green" focusOn=saveAsTitleId onAction=(action 'saveAsPage' page.id)}}
<div class="form-header">
<div class="tip">
<span class="bold">{{folder.name}}:</span> Content Block
</div>
</div>
<div class="input-control">
<label>Name</label>
<div class="tip">Short description to help others understand<br/>this reusable content block</div>
{{textarea rows="3" value=saveAsTitle id=saveAsTitleId}}
</div>
{{/dropdown-dialog}}
{{/if}}
{{/if}}
</div>
</div>

View file

@ -19,5 +19,25 @@
</li>
{{/each}}
</ul>
{{#if hasTemplates}}
<div class="divider"></div>
<div class="template-caption">Published content blocks</div>
<ul class="list">
{{#each templates as |template|}}
<li class="item" {{action 'insertTemplate' template}}>
<div class="icon">
<img class="img" src="/assets/img/section-saved.png" srcset="/assets/img/section-saved@2x.png" />
</div>
<div class="details">
<div class='title'>
{{template.title}}
</div>
<div class='desc'>{{template.firstname}} {{template.lastname}} &middot; {{time-ago template.created}}</div>
</div>
<div class="clearfix" />
</li>
{{/each}}
</ul>
{{/if}}
</div>
</div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 329 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 774 B

117
app/vendor/tether.js vendored
View file

@ -1,4 +1,4 @@
/*! tether 1.3.2 */
/*! tether 1.4.0 */
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
@ -23,6 +23,32 @@ if (typeof TetherBase === 'undefined') {
var zeroElement = null;
// Same as native getBoundingClientRect, except it takes into account parent <frame> offsets
// if the element lies within a nested document (<frame> or <iframe>-like).
function getActualBoundingClientRect(node) {
var boundingRect = node.getBoundingClientRect();
// The original object returned by getBoundingClientRect is immutable, so we clone it
// We can't use extend because the properties are not considered part of the object by hasOwnProperty in IE9
var rect = {};
for (var k in boundingRect) {
rect[k] = boundingRect[k];
}
if (node.ownerDocument !== document) {
var _frameElement = node.ownerDocument.defaultView.frameElement;
if (_frameElement) {
var frameRect = getActualBoundingClientRect(_frameElement);
rect.top += frameRect.top;
rect.bottom += frameRect.top;
rect.left += frameRect.left;
rect.right += frameRect.left;
}
}
return rect;
}
function getScrollParents(el) {
// In firefox if the el is inside an iframe with display: none; window.getComputedStyle() will return null;
// https://bugzilla.mozilla.org/show_bug.cgi?id=548397
@ -51,14 +77,20 @@ function getScrollParents(el) {
var overflowX = _style.overflowX;
var overflowY = _style.overflowY;
if (/(auto|scroll)/.test(overflow + overflowY + overflowX)) {
if (/(auto|scroll|overlay)/.test(overflow + overflowY + overflowX)) {
if (position !== 'absolute' || ['relative', 'absolute', 'fixed'].indexOf(style.position) >= 0) {
parents.push(parent);
}
}
}
parents.push(document.body);
parents.push(el.ownerDocument.body);
// If the node is within a frame, account for the parent window scroll
if (el.ownerDocument !== document) {
parents.push(el.ownerDocument.defaultView);
}
return parents;
}
@ -76,7 +108,7 @@ var getOrigin = function getOrigin() {
// are equivilant or not. We place an element at the top left of the page that will
// get the same jitter, so we can cancel the two out.
var node = zeroElement;
if (!node) {
if (!node || !document.body.contains(node)) {
node = document.createElement('div');
node.setAttribute('data-tether-id', uniqueId());
extend(node.style, {
@ -92,13 +124,7 @@ var getOrigin = function getOrigin() {
var id = node.getAttribute('data-tether-id');
if (typeof zeroPosCache[id] === 'undefined') {
zeroPosCache[id] = {};
var rect = node.getBoundingClientRect();
for (var k in rect) {
// Can't use extend, as on IE9, elements don't resolve to be hasOwnProperty
zeroPosCache[id][k] = rect[k];
}
zeroPosCache[id] = getActualBoundingClientRect(node);
// Clear the cache when this position call is done
defer(function () {
@ -127,13 +153,7 @@ function getBounds(el) {
var docEl = doc.documentElement;
var box = {};
// The original object returned by getBoundingClientRect is immutable, so we clone it
// We can't use extend because the properties are not considered part of the object by hasOwnProperty in IE9
var rect = el.getBoundingClientRect();
for (var k in rect) {
box[k] = rect[k];
}
var box = getActualBoundingClientRect(el);
var origin = getOrigin();
@ -159,7 +179,11 @@ function getOffsetParent(el) {
return el.offsetParent || document.documentElement;
}
var _scrollBarSize = null;
function getScrollBarSize() {
if (_scrollBarSize) {
return _scrollBarSize;
}
var inner = document.createElement('div');
inner.style.width = '100%';
inner.style.height = '200px';
@ -192,7 +216,8 @@ function getScrollBarSize() {
var width = widthContained - widthScroll;
return { width: width, height: width };
_scrollBarSize = { width: width, height: width };
return _scrollBarSize;
}
function extend() {
@ -252,7 +277,9 @@ function hasClass(el, name) {
}
function getClassName(el) {
if (el.className instanceof SVGAnimatedString) {
// Can't use just SVGAnimatedString here since nodes within a Frame in IE have
// completely separately SVGAnimatedString base classes
if (el.className instanceof el.ownerDocument.defaultView.SVGAnimatedString) {
return el.className.baseVal;
}
return el.className;
@ -371,6 +398,7 @@ var Evented = (function () {
})();
TetherBase.Utils = {
getActualBoundingClientRect: getActualBoundingClientRect,
getScrollParents: getScrollParents,
getBounds: getBounds,
getOffsetParent: getOffsetParent,
@ -429,7 +457,7 @@ var transformKey = (function () {
}
var el = document.createElement('div');
var transforms = ['transform', 'webkitTransform', 'OTransform', 'MozTransform', 'msTransform'];
var transforms = ['transform', 'WebkitTransform', 'OTransform', 'MozTransform', 'msTransform'];
for (var i = 0; i < transforms.length; ++i) {
var key = transforms[i];
if (el.style[key] !== undefined) {
@ -828,7 +856,7 @@ var TetherClass = (function (_Evented) {
this.enabled = true;
this.scrollParents.forEach(function (parent) {
if (parent !== document) {
if (parent !== _this3.target.ownerDocument) {
parent.addEventListener('scroll', _this3.position);
}
});
@ -1028,21 +1056,24 @@ var TetherClass = (function (_Evented) {
}
};
var doc = this.target.ownerDocument;
var win = doc.defaultView;
var scrollbarSize = undefined;
if (document.body.scrollWidth > window.innerWidth) {
if (win.innerHeight > doc.documentElement.clientHeight) {
scrollbarSize = this.cache('scrollbar-size', getScrollBarSize);
next.viewport.bottom -= scrollbarSize.height;
}
if (document.body.scrollHeight > window.innerHeight) {
if (win.innerWidth > doc.documentElement.clientWidth) {
scrollbarSize = this.cache('scrollbar-size', getScrollBarSize);
next.viewport.right -= scrollbarSize.width;
}
if (['', 'static'].indexOf(document.body.style.position) === -1 || ['', 'static'].indexOf(document.body.parentElement.style.position) === -1) {
if (['', 'static'].indexOf(doc.body.style.position) === -1 || ['', 'static'].indexOf(doc.body.parentElement.style.position) === -1) {
// Absolute positioning in the body will be relative to the page, not the 'initial containing block'
next.page.bottom = document.body.scrollHeight - top - height;
next.page.right = document.body.scrollWidth - left - width;
next.page.bottom = doc.body.scrollHeight - top - height;
next.page.right = doc.body.scrollWidth - left - width;
}
if (typeof this.options.optimizations !== 'undefined' && this.options.optimizations.moveElement !== false && !(typeof this.targetModifier !== 'undefined')) {
@ -1061,8 +1092,8 @@ var TetherClass = (function (_Evented) {
offsetBorder[side.toLowerCase()] = parseFloat(offsetParentStyle['border' + side + 'Width']);
});
offsetPosition.right = document.body.scrollWidth - offsetPosition.left - offsetParentSize.width + offsetBorder.right;
offsetPosition.bottom = document.body.scrollHeight - offsetPosition.top - offsetParentSize.height + offsetBorder.bottom;
offsetPosition.right = doc.body.scrollWidth - offsetPosition.left - offsetParentSize.width + offsetBorder.right;
offsetPosition.bottom = doc.body.scrollHeight - offsetPosition.top - offsetParentSize.height + offsetBorder.bottom;
if (next.page.top >= offsetPosition.top + offsetBorder.top && next.page.bottom >= offsetPosition.bottom) {
if (next.page.left >= offsetPosition.left + offsetBorder.left && next.page.right >= offsetPosition.right) {
@ -1155,7 +1186,16 @@ var TetherClass = (function (_Evented) {
xPos = -_pos.right;
}
css[transformKey] = 'translateX(' + Math.round(xPos) + 'px) translateY(' + Math.round(yPos) + 'px)';
if (window.matchMedia) {
// HubSpot/tether#207
var retina = window.matchMedia('only screen and (min-resolution: 1.3dppx)').matches || window.matchMedia('only screen and (-webkit-min-device-pixel-ratio: 1.3)').matches;
if (!retina) {
xPos = Math.round(xPos);
yPos = Math.round(yPos);
}
}
css[transformKey] = 'translateX(' + xPos + 'px) translateY(' + yPos + 'px)';
if (transformKey !== 'msTransform') {
// The Z transform will keep this in the GPU (faster, and prevents artifacts),
@ -1207,6 +1247,9 @@ var TetherClass = (function (_Evented) {
}
if (!moved) {
if (this.options.bodyElement) {
this.options.bodyElement.appendChild(this.element);
} else {
var offsetParentIsBody = true;
var currentNode = this.element.parentNode;
while (currentNode && currentNode.nodeType === 1 && currentNode.tagName !== 'BODY') {
@ -1220,7 +1263,8 @@ var TetherClass = (function (_Evented) {
if (!offsetParentIsBody) {
this.element.parentNode.removeChild(this.element);
document.body.appendChild(this.element);
this.element.ownerDocument.body.appendChild(this.element);
}
}
}
@ -1240,6 +1284,7 @@ var TetherClass = (function (_Evented) {
if (write) {
defer(function () {
extend(_this8.element.style, writeCSS);
_this8.trigger('repositioned');
});
}
}
@ -1280,12 +1325,22 @@ function getBoundingRect(tether, to) {
if (typeof to.nodeType !== 'undefined') {
(function () {
var node = to;
var size = getBounds(to);
var pos = size;
var style = getComputedStyle(to);
to = [pos.left, pos.top, size.width + pos.left, size.height + pos.top];
// Account any parent Frames scroll offset
if (node.ownerDocument !== document) {
var win = node.ownerDocument.defaultView;
to[0] += win.pageXOffset;
to[1] += win.pageYOffset;
to[2] += win.pageXOffset;
to[3] += win.pageYOffset;
}
BOUNDS_FORMAT.forEach(function (side, i) {
side = side[0].toUpperCase() + side.substr(1);
if (side === 'Top' || side === 'Left') {

View file

@ -80,10 +80,19 @@ func AddDocumentPage(w http.ResponseWriter, r *http.Request) {
model.Meta.PageID = pageID
model.Page.SetDefaults()
model.Meta.SetDefaults()
model.Meta.OrgID = p.Context.OrgID
model.Meta.UserID = p.Context.UserID
// page.Title = template.HTMLEscapeString(page.Title)
// laod previous meta if page is being created from published template
if model.Page.PresetID != "" {
em, err2 := p.GetPageMeta(model.Page.PresetID)
if err2 != nil {
writeGeneralSQLError(w, method, err2)
return
}
model.Meta = em
model.Meta.PageID = pageID
}
tx, err := request.Db.Beginx()
if err != nil {
@ -111,7 +120,7 @@ func AddDocumentPage(w http.ResponseWriter, r *http.Request) {
log.IfErr(tx.Commit())
newPage, err := p.GetPage(pageID)
newPage, _ := p.GetPage(pageID)
json, err := json.Marshal(newPage)
@ -669,7 +678,7 @@ func GetDocumentRevisions(w http.ResponseWriter, r *http.Request) {
return
}
revisions, err := p.GetDocumentRevisions(documentID)
revisions, _ := p.GetDocumentRevisions(documentID)
payload, err := json.Marshal(revisions)
@ -706,7 +715,7 @@ func GetDocumentPageRevisions(w http.ResponseWriter, r *http.Request) {
return
}
revisions, err := p.GetPageRevisions(pageID)
revisions, _ := p.GetPageRevisions(pageID)
payload, err := json.Marshal(revisions)
@ -757,7 +766,7 @@ func GetDocumentPageDiff(w http.ResponseWriter, r *http.Request) {
return
}
revision, err := p.GetPageRevision(revisionID)
revision, _ := p.GetPageRevision(revisionID)
latestHTML := page.Body
previousHTML := revision.Body
@ -875,3 +884,145 @@ func RollbackDocumentPage(w http.ResponseWriter, r *http.Request) {
writeSuccessBytes(w, payload)
}
/********************
* Page Templates
********************/
type sectionTemplate struct {
DocumentID string `json:"documentId"`
PageID string `json:"pageId"`
Title string `json:"title"`
}
// SavePageAsTemplate inserts new section into document.
func SavePageAsTemplate(w http.ResponseWriter, r *http.Request) {
method := "SavePageAsTemplate"
p := request.GetPersister(r)
defer utility.Close(r.Body)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
writeBadRequestError(w, method, "Bad payload")
return
}
payload := new(sectionTemplate)
err = json.Unmarshal(body, &payload)
if err != nil {
writePayloadError(w, method, err)
return
}
// Data checks
if len(payload.DocumentID) == 0 {
writeMissingDataError(w, method, "documentID")
return
}
if len(payload.PageID) == 0 {
writeMissingDataError(w, method, "pageID")
return
}
if len(payload.Title) == 0 {
writeMissingDataError(w, method, "title")
return
}
if !p.CanChangeDocument(payload.DocumentID) {
writeForbiddenError(w)
return
}
// if strings.HasPrefix(newTitle, "\"") {
// newTitle = newTitle[1:]
// }
// if strings.HasSuffix(newTitle, "\"") {
// newTitle = newTitle[:len(newTitle)-1]
// }
// get previous page
prevPage, err := p.GetPage(payload.PageID)
if err != nil {
writeServerError(w, method, err)
return
}
prevMeta, err := p.GetPageMeta(payload.PageID)
if err != nil {
writeServerError(w, method, err)
return
}
// safety check
if prevPage.DocumentID != payload.DocumentID || prevMeta.DocumentID != payload.DocumentID {
writeUnauthorizedError(w)
return
}
newID := util.UniqueID()
prevPage.RefID = newID
prevPage.Preset = true
prevPage.Title = payload.Title
prevMeta.PageID = newID
tx, err := request.Db.Beginx()
if err != nil {
writeTransactionError(w, method, err)
return
}
p.Context.Transaction = tx
model := new(models.PageModel)
model.Page = prevPage
model.Meta = prevMeta
err = p.AddPage(*model)
if err != nil {
log.IfErr(tx.Rollback())
writeGeneralSQLError(w, method, err)
return
}
log.IfErr(tx.Commit())
writeSuccessEmptyJSON(w)
}
// GetSpaceSectionTemplates gets published section templates
func GetSpaceSectionTemplates(w http.ResponseWriter, r *http.Request) {
method := "GetSpaceSectionTemplates"
p := request.GetPersister(r)
params := mux.Vars(r)
folderID := params["folderID"]
if len(folderID) == 0 {
writeMissingDataError(w, method, "folderID")
return
}
var pages []entity.PageTemplate
var err error
pages, err = p.GetSpaceSectionTemplates(folderID)
if len(pages) == 0 {
pages = []entity.PageTemplate{}
}
if err != nil {
writeGeneralSQLError(w, method, err)
return
}
json, err := json.Marshal(pages)
if err != nil {
writeJSONMarshalError(w, method, "page", err)
return
}
writeSuccessBytes(w, json)
}

View file

@ -216,6 +216,8 @@ func init() {
log.IfErr(Add(RoutePrefixPrivate, "sections", []string{"GET", "OPTIONS"}, nil, GetSections))
log.IfErr(Add(RoutePrefixPrivate, "sections", []string{"POST", "OPTIONS"}, nil, RunSectionCommand))
log.IfErr(Add(RoutePrefixPrivate, "sections/refresh", []string{"GET", "OPTIONS"}, nil, RefreshSections))
log.IfErr(Add(RoutePrefixPrivate, "sections/templates/{folderID}", []string{"GET", "OPTIONS"}, nil, GetSpaceSectionTemplates))
log.IfErr(Add(RoutePrefixPrivate, "sections/templates", []string{"POST", "OPTIONS"}, nil, SavePageAsTemplate))
// Links
log.IfErr(Add(RoutePrefixPrivate, "links/{folderID}/{documentID}/{pageID}", []string{"GET", "OPTIONS"}, nil, GetLinkCandidates))

View file

@ -186,6 +186,8 @@ type Page struct {
UserID string `json:"userId"`
ContentType string `json:"contentType"`
PageType string `json:"pageType"`
Preset bool `json:"preset"`
PresetID string `json:"presetId"`
Level uint64 `json:"level"`
Sequence float64 `json:"sequence"`
Title string `json:"title"`
@ -259,6 +261,14 @@ type Revision struct {
Revisions int `json:"revisions"`
}
// PageTemplate represents a section that has been published as a template.
// We have to create this struct to hold user name.
type PageTemplate struct {
Page
Firstname string `json:"firstname"`
Lastname string `json:"lastname"`
}
// DocumentMeta details who viewed the document.
type DocumentMeta struct {
Viewers []DocumentMetaViewer `json:"viewers"`

View file

@ -26,7 +26,6 @@ import (
// AddPage inserts the given page into the page table, adds that page to the queue of pages to index and audits that the page has been added.
func (p *Persister) AddPage(model models.PageModel) (err error) {
err = nil
model.Page.OrgID = p.Context.OrgID
model.Page.UserID = p.Context.UserID
model.Page.Created = time.Now().UTC()
@ -51,7 +50,7 @@ func (p *Persister) AddPage(model models.PageModel) (err error) {
model.Page.Sequence = maxSeq * 2
}
stmt, err := p.Context.Transaction.Preparex("INSERT INTO page (refid, orgid, documentid, userid, contenttype, pagetype, level, title, body, revisions, sequence, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
stmt, err := p.Context.Transaction.Preparex("INSERT INTO page (refid, orgid, documentid, userid, contenttype, pagetype, level, title, body, revisions, sequence, preset, presetid, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
defer utility.Close(stmt)
if err != nil {
@ -59,14 +58,14 @@ func (p *Persister) AddPage(model models.PageModel) (err error) {
return
}
_, err = stmt.Exec(model.Page.RefID, model.Page.OrgID, model.Page.DocumentID, model.Page.UserID, model.Page.ContentType, model.Page.PageType, model.Page.Level, model.Page.Title, model.Page.Body, model.Page.Revisions, model.Page.Sequence, model.Page.Created, model.Page.Revised)
_, err = stmt.Exec(model.Page.RefID, model.Page.OrgID, model.Page.DocumentID, model.Page.UserID, model.Page.ContentType, model.Page.PageType, model.Page.Level, model.Page.Title, model.Page.Body, model.Page.Revisions, model.Page.Sequence, model.Page.Preset, model.Page.PresetID, model.Page.Created, model.Page.Revised)
if err != nil {
log.Error("Unable to execute insert for page", err)
return
}
err = searches.Add(&databaseRequest{OrgID: p.Context.OrgID}, model.Page, model.Page.RefID)
_ = searches.Add(&databaseRequest{OrgID: p.Context.OrgID}, model.Page, model.Page.RefID)
stmt2, err := p.Context.Transaction.Preparex("INSERT INTO pagemeta (pageid, orgid, userid, documentid, rawbody, config, externalsource, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)")
defer utility.Close(stmt2)
@ -92,7 +91,7 @@ func (p *Persister) AddPage(model models.PageModel) (err error) {
func (p *Persister) GetPage(pageID string) (page entity.Page, err error) {
err = nil
stmt, err := Db.Preparex("SELECT a.id, a.refid, a.orgid, a.documentid, a.userid, a.contenttype, a.pagetype, a.level, a.sequence, a.title, a.body, a.revisions, a.created, a.revised FROM page a WHERE a.orgid=? AND a.refid=?")
stmt, err := Db.Preparex("SELECT a.id, a.refid, a.orgid, a.documentid, a.userid, a.contenttype, a.pagetype, a.level, a.sequence, a.title, a.body, a.revisions, a.preset, a.presetid, a.created, a.revised FROM page a WHERE a.orgid=? AND a.refid=?")
defer utility.Close(stmt)
if err != nil {
@ -114,7 +113,7 @@ func (p *Persister) GetPage(pageID string) (page entity.Page, err error) {
func (p *Persister) GetPages(documentID string) (pages []entity.Page, err error) {
err = nil
err = Db.Select(&pages, "SELECT a.id, a.refid, a.orgid, a.documentid, a.userid, a.contenttype, a.pagetype, a.level, a.sequence, a.title, a.body, a.revisions, a.created, a.revised FROM page a WHERE a.orgid=? AND a.documentid=? ORDER BY a.sequence", p.Context.OrgID, documentID)
err = Db.Select(&pages, "SELECT a.id, a.refid, a.orgid, a.documentid, a.userid, a.contenttype, a.pagetype, a.level, a.sequence, a.title, a.body, a.revisions, a.preset, a.presetid, a.created, a.revised FROM page a WHERE a.orgid=? AND a.documentid=? ORDER BY a.sequence", p.Context.OrgID, documentID)
if err != nil {
log.Error(fmt.Sprintf("Unable to execute select pages for org %s and document %s", p.Context.OrgID, documentID), err)
@ -131,7 +130,7 @@ func (p *Persister) GetPagesWhereIn(documentID, inPages string) (pages []entity.
args := []interface{}{p.Context.OrgID, documentID}
tempValues := strings.Split(inPages, ",")
sql := "SELECT a.id, a.refid, a.orgid, a.documentid, a.userid, a.contenttype, a.pagetype, a.level, a.sequence, a.title, a.body, a.revisions, a.created, a.revised FROM page a WHERE a.orgid=? AND a.documentid=? AND a.refid IN (?" + strings.Repeat(",?", len(tempValues)-1) + ") ORDER BY sequence"
sql := "SELECT a.id, a.refid, a.orgid, a.documentid, a.userid, a.contenttype, a.pagetype, a.level, a.sequence, a.title, a.body, a.preset, a.presetid, a.revisions, a.created, a.revised FROM page a WHERE a.orgid=? AND a.documentid=? AND a.refid IN (?" + strings.Repeat(",?", len(tempValues)-1) + ") ORDER BY sequence"
inValues := make([]interface{}, len(tempValues))
@ -181,7 +180,7 @@ func (p *Persister) GetPagesWhereIn(documentID, inPages string) (pages []entity.
// GetPagesWithoutContent returns a slice containing all the page records for a given documentID, in presentation sequence,
// but without the body field (which holds the HTML content).
func (p *Persister) GetPagesWithoutContent(documentID string) (pages []entity.Page, err error) {
err = Db.Select(&pages, "SELECT id, refid, orgid, documentid, userid, contenttype, pagetype, sequence, level, title, revisions, created, revised FROM page WHERE orgid=? AND documentid=? ORDER BY sequence", p.Context.OrgID, documentID)
err = Db.Select(&pages, "SELECT id, refid, orgid, documentid, userid, contenttype, pagetype, sequence, level, title, revisions, preset, presetid, created, revised FROM page WHERE orgid=? AND documentid=? ORDER BY sequence", p.Context.OrgID, documentID)
if err != nil {
log.Error(fmt.Sprintf("Unable to execute select pages for org %s and document %s", p.Context.OrgID, documentID), err)
@ -386,17 +385,17 @@ func (p *Persister) DeletePage(documentID, pageID string) (rows int64, err error
rows, err = p.Base.DeleteConstrained(p.Context.Transaction, "page", p.Context.OrgID, pageID)
if err == nil {
_, err = p.Base.DeleteWhere(p.Context.Transaction, fmt.Sprintf("DELETE FROM pagemeta WHERE orgid='%s' AND pageid='%s'", p.Context.OrgID, pageID))
_, err = searches.Delete(&databaseRequest{OrgID: p.Context.OrgID}, documentID, pageID)
_, _ = p.Base.DeleteWhere(p.Context.Transaction, fmt.Sprintf("DELETE FROM pagemeta WHERE orgid='%s' AND pageid='%s'", p.Context.OrgID, pageID))
_, _ = searches.Delete(&databaseRequest{OrgID: p.Context.OrgID}, documentID, pageID)
// delete content links from this page
_, err = p.DeleteSourcePageLinks(pageID)
_, _ = p.DeleteSourcePageLinks(pageID)
// mark as orphan links to this page
err = p.MarkOrphanPageLink(pageID)
_ = p.MarkOrphanPageLink(pageID)
// nuke revisions
_, err = p.DeletePageRevisions(pageID)
_, _ = p.DeletePageRevisions(pageID)
p.Base.Audit(p.Context, "remove-page", documentID, pageID)
}
@ -508,3 +507,24 @@ func (p *Persister) DeletePageRevisions(pageID string) (rows int64, err error) {
return
}
/********************
* Section templates
********************/
// GetSpaceSectionTemplates returns a slice all saved section templates.
func (p *Persister) GetSpaceSectionTemplates(folderID string) (pages []entity.PageTemplate, err error) {
err = Db.Select(&pages, `SELECT a.id, a.refid, a.orgid, a.documentid, a.userid, a.contenttype, a.pagetype, a.level, a.sequence, a.title, a.body, a.revisions, a.preset, a.presetid, a.created, a.revised, u.firstname, u.lastname
FROM page a
LEFT JOIN document b ON a.documentid = b.refid
LEFT JOIN user u ON a.userid = u.refid
WHERE a.orgid=? AND b.labelid=? AND a.preset=1
ORDER BY a.title`, p.Context.OrgID, folderID)
if err != nil {
log.Error(fmt.Sprintf("Unable to execute GetSpaceSectionTemplates for org %s and space %s", p.Context.OrgID, folderID), err)
return
}
return
}

View file

@ -0,0 +1,10 @@
/* community edition */
ALTER TABLE page ADD COLUMN `preset` BOOL NOT NULL DEFAULT 0 AFTER `pagetype`;
ALTER TABLE page ADD COLUMN `presetid` CHAR(16) NOT NULL DEFAULT '' COLLATE utf8_bin AFTER `preset`;
/* Note:
Preset data is not required in pagemeta as a simple join to page will surface these fields.
Version history table does not need these fields as they are populated once during page creation:
-- you cannot mark an existing section as a preset
-- a page is only marked as preset during it's creation (e.g. created from an existing preset)
*/

File diff suppressed because one or more lines are too long